Skip to content

Commit e0e590f

Browse files
committed
Drop support for Python 3.5
1 parent d5c400a commit e0e590f

File tree

3 files changed

+52
-52
lines changed

3 files changed

+52
-52
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Changelog
88
* Use a Web session for JSON-RPC requests
99
when Requests is installed.
1010

11+
* Drop support for Python 3.5
12+
1113

1214
2.2.1 (2025-09-24)
1315
~~~~~~~~~~~~~~~~~~

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Key features:
3232
- simplified syntax for search ``domain`` and ``fields``
3333
- full API accessible on the ``Client.env`` environment
3434
- the module can be imported and used as a library: ``from odooly import Client``
35-
- supports Python 3.5 and above
35+
- supports Python 3.6 and above
3636

3737

3838

odooly.py

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def format_exception(exc_type, exc, tb, limit=None, chain=True,
211211
message = str(server_error['arguments'][0])
212212
except Exception:
213213
message = str(server_error['arguments'])
214-
fault = '%s: %s' % (server_error['name'], message)
214+
fault = f"{server_error['name']}: {message}"
215215
exc_type = server_error.get('exception_type', 'internal_error')
216216
if exc_type != 'internal_error' or message.startswith('FATAL:'):
217217
server_tb = None
@@ -247,7 +247,7 @@ def read_config(section=None):
247247
server = shlex.split(env.get('options', ''))
248248
else:
249249
protocol = env.get('protocol', 'xmlrpc')
250-
server = '%s://%s:%s/%s' % (scheme, env['host'], env['port'], protocol)
250+
server = f"{scheme}://{env['host']}:{env['port']}/{protocol}"
251251
return (server, env['database'], env['username'], env.get('password'))
252252

253253

@@ -320,7 +320,7 @@ def searchargs(params, kwargs=None):
320320
if isinstance(term, str) and term not in DOMAIN_OPERATORS:
321321
m = _term_re.match(term.strip())
322322
if not m:
323-
raise ValueError('Cannot parse term %r' % term)
323+
raise ValueError(f"Cannot parse term {term!r}")
324324
(field, operator, value) = m.groups()
325325
try:
326326
value = literal_eval(value)
@@ -366,7 +366,7 @@ def dispatch_jsonrpc(url, service_name, method, args):
366366
'jsonrpc': '2.0',
367367
'method': 'call',
368368
'params': {'service': service_name, 'method': method, 'args': args},
369-
'id': '%04x%010x' % (os.getpid(), (int(time.time() * 1E6) % 2**40)),
369+
'id': f"{os.getpid():04x}{int(time.time() * 1E6) % 2**40:010x}",
370370
}
371371
resp = http_post(url, json.dumps(data).encode('ascii'))
372372
if resp.get('error'):
@@ -379,7 +379,7 @@ class partial(functools.partial):
379379

380380
def __repr__(self):
381381
# Hide arguments
382-
return '%s(%r, ...)' % (self.__class__.__name__, self.func)
382+
return f"{self.__class__.__name__}({self.func!r}, ...)"
383383

384384

385385
class Error(Exception):
@@ -411,15 +411,15 @@ def __init__(self, client, endpoint, methods, verbose=False):
411411
self._verbose = verbose
412412

413413
def __repr__(self):
414-
return "<Service '%s|%s'>" % (self._rpcpath, self._endpoint)
414+
return f"<Service '{self._rpcpath}|{self._endpoint}'>"
415415
__str__ = __repr__
416416

417417
def __dir__(self):
418418
return sorted(self._methods)
419419

420420
def __getattr__(self, name):
421421
if name not in self._methods:
422-
raise AttributeError("'Service' object has no attribute %r" % name)
422+
raise AttributeError(f"'Service' object has no attribute {name!r}")
423423
if self._verbose:
424424
def sanitize(args):
425425
if self._endpoint != 'db' and len(args) > 2:
@@ -430,17 +430,17 @@ def sanitize(args):
430430

431431
def wrapper(self, *args):
432432
snt = ', '.join([repr(arg) for arg in sanitize(args)])
433-
snt = '%s.%s(%s)' % (self._endpoint, name, snt)
433+
snt = f"{self._endpoint}.{name}({snt})"
434434
if len(snt) > maxcol:
435-
suffix = '... L=%s' % len(snt)
435+
suffix = f"... L={len(snt)}"
436436
snt = snt[:maxcol - len(suffix)] + suffix
437-
print('--> ' + snt)
437+
print(f"--> {snt}")
438438
res = self._dispatch(name, args)
439439
rcv = str(res)
440440
if len(rcv) > maxcol:
441-
suffix = '... L=%s' % len(rcv)
441+
suffix = f"... L={len(rcv)}"
442442
rcv = rcv[:maxcol - len(suffix)] + suffix
443-
print('<-- ' + rcv)
443+
print(f"<-- {rcv}")
444444
return res
445445
else:
446446
wrapper = lambda s, *args: s._dispatch(name, args)
@@ -497,8 +497,7 @@ def __bool__(self):
497497
__hash__ = object.__hash__
498498

499499
def __repr__(self):
500-
return "<Env '%s@%s'>" % (self.user.login if self.uid else '',
501-
self.db_name)
500+
return f"<Env '{self.user.login if self.uid else ''}@{self.db_name}'>"
502501

503502
def check_uid(self, uid, password):
504503
"""Check if ``(uid, password)`` is valid.
@@ -541,10 +540,10 @@ def _auth(self, user, password):
541540
if not password and uid is not False:
542541
from getpass import getpass
543542
if user is None:
544-
name = 'admin' if uid == SUPERUSER_ID else ('UID %d' % uid)
543+
name = 'admin' if uid == SUPERUSER_ID else f'UID {uid}'
545544
else:
546545
name = user
547-
password = getpass('Password for %r: ' % name)
546+
password = getpass(f"Password for {name!r}: ")
548547
# Check if password is valid
549548
uid = self.check_uid(uid, password) if (uid and not verified) else uid
550549
if uid is None:
@@ -784,7 +783,7 @@ def _get(self, name, check=True):
784783
if model_names:
785784
errmsg = 'Model not found. These models exist:'
786785
else:
787-
errmsg = 'Model not found: %s' % (name,)
786+
errmsg = f'Model not found: {name}'
788787
raise Error('\n * '.join([errmsg] + model_names))
789788

790789
def modules(self, name='', installed=None):
@@ -820,23 +819,23 @@ def _upgrade(self, modules, button):
820819
ir_module = self._get('ir.module.module', False)
821820
updated, added = ir_module.update_list()
822821
if added:
823-
print('%s module(s) added to the list' % added)
822+
print(f'{added} module(s) added to the list')
824823
# Find modules
825824
sel = modules and ir_module.search([('name', 'in', modules)])
826825
mods = ir_module.read([_pending_state], 'name state')
827826
if sel:
828827
# Safety check
829828
if any(mod['name'] not in modules for mod in mods):
830829
raise Error('Pending actions:\n' + '\n'.join(
831-
(' %(state)s\t%(name)s' % mod) for mod in mods))
830+
f" {mod['state']}\t{mod['name']}" for mod in mods))
832831
if button == 'button_uninstall':
833832
# Safety check
834833
names = ir_module.read([('id', 'in', sel.ids),
835834
'state != installed',
836835
'state != to upgrade',
837836
'state != to remove'], 'name')
838837
if names:
839-
raise Error('Not installed: %s' % ', '.join(names))
838+
raise Error(f"Not installed: {', '.join(names)}")
840839
if self.client.version_info < 7.0:
841840
# A trick to uninstall dependent add-ons
842841
sel.write({'state': 'to remove'})
@@ -849,13 +848,13 @@ def _upgrade(self, modules, button):
849848
print('Already up-to-date: %s' %
850849
self.modules([('id', 'in', sel.ids)]))
851850
elif modules:
852-
raise Error('Module(s) not found: %s' % ', '.join(modules))
853-
print('%s module(s) updated' % updated)
851+
raise Error(f"Module(s) not found: {', '.join(modules)}")
852+
print(f'{updated} module(s) updated')
854853
return
855-
print('%s module(s) selected' % len(sel))
856-
print('%s module(s) to process:' % len(mods))
854+
print(f'{len(sel)} module(s) selected')
855+
print(f'{len(mods)} module(s) to process:')
857856
for mod in mods:
858-
print(' %(state)s\t%(name)s' % mod)
857+
print(f" {mod['state']}\t{mod['name']}")
859858

860859
# Empty the cache for this database
861860
self.refresh()
@@ -943,7 +942,7 @@ def get_service(name):
943942
self.server_version = ver = get_service('db').server_version()
944943
self.major_version = re.search(r'\d+\.?\d*', ver).group()
945944
self.version_info = float_version = float(self.major_version)
946-
assert float_version > 6.0, 'Not supported: %s' % ver
945+
assert float_version > 6.0, f'Not supported: {ver}'
947946
# Create the RPC services
948947
self.db = get_service('db')
949948
self.common = get_service('common')
@@ -983,7 +982,7 @@ def from_config(cls, environment, user=None, verbose=False):
983982
return client
984983

985984
def __repr__(self):
986-
return "<Client '%s#%s'>" % (self._server, self.env.db_name)
985+
return f"<Client '{self._server}#{self.env.db_name}'>"
987986

988987
def close(self):
989988
for conn in self._connections:
@@ -1026,7 +1025,7 @@ def login(self, user, password=None, database=None):
10261025
try:
10271026
self._login(user, password=password, database=database)
10281027
except Error as exc:
1029-
print('%s: %s' % (exc.__class__.__name__, exc))
1028+
print(f"{exc.__class__.__name__}: {exc}")
10301029
else:
10311030
# Register the new globals()
10321031
self.connect()
@@ -1043,11 +1042,11 @@ def connect(self, env_name=None):
10431042
self._globals['env'] = client.env
10441043
self._globals['self'] = client.env.user if client.env.uid else None
10451044
# Tweak prompt
1046-
sys.ps1 = '%s >>> ' % (env_name,)
1045+
sys.ps1 = f'{env_name} >>> '
10471046
sys.ps2 = '... '.rjust(len(sys.ps1))
10481047
# Logged in?
10491048
if client.env.uid:
1050-
print('Logged in as %r' % (client.env.user.login,))
1049+
print(f'Logged in as {client.env.user.login!r}')
10511050

10521051
@classmethod
10531052
def _set_interactive(cls, global_vars={}):
@@ -1138,7 +1137,7 @@ def _new(cls, env, name):
11381137
return m
11391138

11401139
def __repr__(self):
1141-
return "<Model '%s'>" % (self._name,)
1140+
return f"<Model '{self._name}'>"
11421141

11431142
def keys(self):
11441143
"""Return the keys of the model."""
@@ -1220,12 +1219,12 @@ def get(self, domain, *args, **kwargs):
12201219
rec = self.env.ref(domain)
12211220
if not rec:
12221221
return None
1223-
assert rec._model is self, 'Model mismatch %r %r' % (rec, self)
1222+
assert rec._model is self, f'Model mismatch {rec!r} {self!r}'
12241223
return rec
12251224
assert issearchdomain(domain) # a search domain
12261225
ids = self._execute('search', domain)
12271226
if len(ids) > 1:
1228-
raise ValueError('domain matches too many records (%d)' % len(ids))
1227+
raise ValueError(f'domain matches too many records ({len(ids)})')
12291228
return Record(self, ids[0]) if ids else None
12301229

12311230
def create(self, values):
@@ -1327,7 +1326,7 @@ def _unbrowse_values(self, values):
13271326
field_type = self._fields[key]['type']
13281327
if hasattr(value, 'id'):
13291328
if field_type == 'reference':
1330-
new_values[key] = '%s,%s' % (value._name, value.id)
1329+
new_values[key] = f'{value._name},{value.id}'
13311330
else:
13321331
new_values[key] = value = value.id
13331332
if field_type in ('one2many', 'many2many'):
@@ -1350,7 +1349,7 @@ def _get_external_ids(self, ids=None):
13501349
['module', 'name', 'res_id'])
13511350
res = {}
13521351
for rec in existing:
1353-
res['%(module)s.%(name)s' % rec] = self.get(rec['res_id'])
1352+
res[f"{rec['module']}.{rec['name']}"] = self.get(rec['res_id'])
13541353
return res
13551354

13561355
def __getattr__(self, attr):
@@ -1363,7 +1362,7 @@ def __getattr__(self, attr):
13631362
if attr == '_keys':
13641363
return _memoize(self, attr, sorted(self._fields))
13651364
if attr.startswith('_'):
1366-
raise AttributeError("'Model' object has no attribute %r" % attr)
1365+
raise AttributeError(f"'Model' object has no attribute {attr!r}")
13671366

13681367
def wrapper(self, *params, **kwargs):
13691368
"""Wrapper for client.execute(%r, %r, *params, **kwargs)."""
@@ -1412,11 +1411,10 @@ def __new__(cls, res_model, arg):
14121411

14131412
def __repr__(self):
14141413
if len(self.ids) > 16:
1415-
ids = 'length=%d' % len(self.ids)
1414+
ids = f'length={len(self.ids)}'
14161415
else:
14171416
ids = self.id
1418-
return "<%s '%s,%s'>" % (self.__class__.__name__,
1419-
self._name, ids)
1417+
return f"<{self.__class__.__name__} '{self._name},{ids}'>"
14201418

14211419
def __dir__(self):
14221420
attrs = set(self.__dict__) | set(self._model._keys)
@@ -1511,7 +1509,7 @@ def ensure_one(self):
15111509
recs = self.union()
15121510
if len(recs.id) == 1:
15131511
return recs[0]
1514-
raise ValueError("Expected singleton: %s" % self)
1512+
raise ValueError(f"Expected singleton: {self}")
15151513

15161514
def exists(self):
15171515
"""Return a subset of records that exist."""
@@ -1720,7 +1718,7 @@ def __getattr__(self, attr):
17201718
if attr in self._model._keys:
17211719
return self.read(attr)
17221720
if attr.startswith('_'):
1723-
errmsg = "'RecordList' object has no attribute %r" % attr
1721+
errmsg = f"'RecordList' object has no attribute {attr!r}"
17241722
raise AttributeError(errmsg)
17251723

17261724
def wrapper(self, *params, **kwargs):
@@ -1730,10 +1728,10 @@ def wrapper(self, *params, **kwargs):
17301728

17311729
def __setattr__(self, attr, value):
17321730
if attr in self._model._keys or attr == 'id':
1733-
msg = "attribute %r is read-only; use 'RecordList.write' instead."
1731+
msg = f"attribute {attr!r} is read-only; use 'RecordList.write' instead."
17341732
else:
1735-
msg = "has no attribute %r"
1736-
raise AttributeError("'RecordList' object %s" % msg % attr)
1733+
msg = f"has no attribute {attr!r}"
1734+
raise AttributeError("'RecordList' object " + msg)
17371735

17381736
def __eq__(self, other):
17391737
return (isinstance(other, RecordList) and
@@ -1759,9 +1757,9 @@ def __str__(self):
17591757
def _get_name(self):
17601758
try:
17611759
(id_name,) = self._execute('name_get', [self.id])
1762-
name = '%s' % (id_name[1],)
1760+
name = f'{id_name[1]}'
17631761
except Exception:
1764-
name = '%s,%d' % (self._name, self.id)
1762+
name = f'{self._name},{self.id}'
17651763
self.__dict__['_idnames'] = [(self.id, name)]
17661764
return _memoize(self, '_Record__name', name)
17671765

@@ -1827,7 +1825,7 @@ def _set_external_id(self, xml_id):
18271825
domain = ['|', '&', ('module', '=', mod), ('name', '=', name),
18281826
'&', ('model', '=', self._name), ('res_id', '=', self.id)]
18291827
if self.env['ir.model.data'].search(domain):
1830-
raise ValueError('ID %r collides with another entry' % xml_id)
1828+
raise ValueError(f'ID {xml_id!r} collides with another entry')
18311829
self.env['ir.model.data'].create({
18321830
'model': self._name,
18331831
'res_id': self.id,
@@ -1841,7 +1839,7 @@ def __getattr__(self, attr):
18411839
if attr == '_Record__name':
18421840
return self._get_name()
18431841
if attr.startswith('_'):
1844-
raise AttributeError("'Record' object has no attribute %r" % attr)
1842+
raise AttributeError(f"'Record' object has no attribute {attr!r}")
18451843

18461844
def wrapper(self, *params, **kwargs):
18471845
"""Wrapper for client.execute(%r, %r, %d, *params, **kwargs)."""
@@ -1856,7 +1854,7 @@ def __setattr__(self, attr, value):
18561854
if attr == '_external_id':
18571855
return self._set_external_id(value)
18581856
if attr not in self._model._keys:
1859-
raise AttributeError("'Record' object has no attribute %r" % attr)
1857+
raise AttributeError(f"'Record' object has no attribute {attr!r}")
18601858
if attr == 'id':
18611859
raise AttributeError("'Record' object attribute 'id' is read-only")
18621860
self.write({attr: value})
@@ -1933,10 +1931,10 @@ def main(interact=_interact):
19331931
help='read connection settings from the given section')
19341932
parser.add_option(
19351933
'-c', '--config', default=None,
1936-
help='specify alternate config file (default: %r)' % CONF_FILE)
1934+
help=f'specify alternate config file (default: {CONF_FILE!r})')
19371935
parser.add_option(
19381936
'--server', default=None,
1939-
help='full URL of the server (default: %s)' % DEFAULT_URL)
1937+
help=f'full URL of the server (default: {DEFAULT_URL})')
19401938
parser.add_option('-d', '--db', default=DEFAULT_DB, help='database')
19411939
parser.add_option('-u', '--user', default=None, help='username')
19421940
parser.add_option(

0 commit comments

Comments
 (0)