Odooly 2.6.2__tar.gz → 2.6.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {odooly-2.6.2 → odooly-2.6.3}/CHANGES.rst +6 -0
  2. {odooly-2.6.2 → odooly-2.6.3/Odooly.egg-info}/PKG-INFO +1 -1
  3. {odooly-2.6.2/Odooly.egg-info → odooly-2.6.3}/PKG-INFO +1 -1
  4. {odooly-2.6.2 → odooly-2.6.3}/odooly.py +32 -36
  5. odooly-2.6.3/odooly_run.py +164 -0
  6. odooly-2.6.2/odooly_run.py +0 -131
  7. {odooly-2.6.2 → odooly-2.6.3}/LICENSE +0 -0
  8. {odooly-2.6.2 → odooly-2.6.3}/MANIFEST.in +0 -0
  9. {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/SOURCES.txt +0 -0
  10. {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/dependency_links.txt +0 -0
  11. {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/entry_points.txt +0 -0
  12. {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/top_level.txt +0 -0
  13. {odooly-2.6.2 → odooly-2.6.3}/README.rst +0 -0
  14. {odooly-2.6.2 → odooly-2.6.3}/docs/Makefile +0 -0
  15. {odooly-2.6.2 → odooly-2.6.3}/docs/api.rst +0 -0
  16. {odooly-2.6.2 → odooly-2.6.3}/docs/conf.py +0 -0
  17. {odooly-2.6.2 → odooly-2.6.3}/docs/developer.rst +0 -0
  18. {odooly-2.6.2 → odooly-2.6.3}/docs/index.rst +0 -0
  19. {odooly-2.6.2 → odooly-2.6.3}/docs/intro.rst +0 -0
  20. {odooly-2.6.2 → odooly-2.6.3}/docs/make.bat +0 -0
  21. {odooly-2.6.2 → odooly-2.6.3}/docs/tutorial.rst +0 -0
  22. {odooly-2.6.2 → odooly-2.6.3}/odooly.ini +0 -0
  23. {odooly-2.6.2 → odooly-2.6.3}/setup.cfg +0 -0
  24. {odooly-2.6.2 → odooly-2.6.3}/setup.py +0 -0
  25. {odooly-2.6.2 → odooly-2.6.3}/tests/__init__.py +0 -0
  26. {odooly-2.6.2 → odooly-2.6.3}/tests/_common.py +0 -0
  27. {odooly-2.6.2 → odooly-2.6.3}/tests/test_client.py +0 -0
  28. {odooly-2.6.2 → odooly-2.6.3}/tests/test_interact.py +0 -0
  29. {odooly-2.6.2 → odooly-2.6.3}/tests/test_model.py +0 -0
  30. {odooly-2.6.2 → odooly-2.6.3}/tests/test_util.py +0 -0
@@ -2,6 +2,12 @@ Changelog
2
2
  ---------
3
3
 
4
4
 
5
+ 2.6.3 (2026-03-18)
6
+ ~~~~~~~~~~~~~~~~~~
7
+
8
+ * Improve output coloring. Do not color Python keywords and built-ins.
9
+
10
+
5
11
  2.6.2 (2026-03-17)
6
12
  ~~~~~~~~~~~~~~~~~~
7
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Odooly
3
- Version: 2.6.2
3
+ Version: 2.6.3
4
4
  Summary: Versatile tool for Odoo
5
5
  Home-page: http://odooly.readthedocs.org/
6
6
  Author: Florent Xicluna
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Odooly
3
- Version: 2.6.2
3
+ Version: 2.6.3
4
4
  Summary: Versatile tool for Odoo
5
5
  Home-page: http://odooly.readthedocs.org/
6
6
  Author: Florent Xicluna
@@ -28,7 +28,7 @@ try:
28
28
  except ImportError:
29
29
  requests = None
30
30
 
31
- __version__ = '2.6.2'
31
+ __version__ = '2.6.3'
32
32
  __all__ = ['Client', 'Env', 'HTTPSession', 'WebAPI', 'Service', 'Json2',
33
33
  'Printer', 'Error', 'ServerError',
34
34
  'BaseModel', 'Model', 'BaseRecord', 'Record', 'RecordList',
@@ -132,7 +132,7 @@ for query in sql_queries:
132
132
  log(str({'queries': sql_queries, 'result': result}))
133
133
  result[:] = []
134
134
  """
135
- colorize = color_bold = color_comment = str
135
+ color_bold = color_comment = color_py = color_repr = str
136
136
  http_context = None
137
137
 
138
138
  if os.getenv('ODOOLY_SSL_UNVERIFIED'):
@@ -225,12 +225,12 @@ def _memoize(inst, attr, value, doc_values=None):
225
225
 
226
226
  # Simplified ast.literal_eval which does not parse operators
227
227
  def _convert(node):
228
- if isinstance(node, _ast.Constant):
228
+ if isinstance(node, _ast.Constant) and node.value.__class__ is not complex:
229
229
  return node.value
230
230
  if isinstance(node, _ast.Tuple):
231
231
  return tuple(map(_convert, node.elts))
232
232
  if isinstance(node, _ast.List):
233
- return list(map(_convert, node.elts))
233
+ return [*map(_convert, node.elts)]
234
234
  if isinstance(node, _ast.Dict):
235
235
  return {_convert(k): _convert(v)
236
236
  for (k, v) in zip(node.keys, node.values)}
@@ -242,10 +242,8 @@ def _convert(node):
242
242
  raise ValueError('malformed or disallowed expression')
243
243
 
244
244
 
245
- def literal_eval(expression, _octal_digits=frozenset('01234567')):
245
+ def literal_eval(expression):
246
246
  node = compile(expression, '<unknown>', 'eval', _ast.PyCF_ONLY_AST)
247
- if expression[:1] == '0' and expression[1:2] in _octal_digits:
248
- raise SyntaxError('unsupported octal notation')
249
247
  return _convert(node.body)
250
248
 
251
249
 
@@ -419,7 +417,7 @@ class Printer:
419
417
  xch = message[:self.cols - len(suffix)] + suffix
420
418
  print(color_comment(f"{_prefix} {xch}"))
421
419
 
422
- print_sent = functools.partialmethod(_print_, _prefix='-->')
420
+ print_sent = functools.partialmethod(_print_, _prefix=' >')
423
421
  print_recv = functools.partialmethod(_print_, _prefix='<--')
424
422
 
425
423
  def __bool__(self):
@@ -452,7 +450,7 @@ class WebAPI:
452
450
  self._dispatch = client._proxy_web(endpoint)
453
451
  self._server = urljoin(client._server, '/')
454
452
  self._endpoint = f'/{endpoint}'
455
- self._methods = methods
453
+ self._methods = [*methods]
456
454
  self._printer = client._printer
457
455
 
458
456
  def __repr__(self):
@@ -802,7 +800,7 @@ class Env:
802
800
 
803
801
  def refresh(self):
804
802
  db_key, preserve = (self.db_name, self.client._server), ('auth', Env)
805
- for key in list(self._cache):
803
+ for key in [*self._cache]:
806
804
  if key[1:] == db_key and key[0] not in preserve and self._cache[key] != self:
807
805
  del self._cache[key]
808
806
  self._access_models = None
@@ -884,7 +882,7 @@ class Env:
884
882
  method, [ids] = 'search_read', searchargs(params[:1])
885
883
  else:
886
884
  order_ids = kwargs.pop('order', False) and params[0]
887
- ids = sorted(set(params[0]) - {False})
885
+ ids = sorted({*params[0]} - {False})
888
886
  if not ids and order_ids:
889
887
  return [False] * len(order_ids)
890
888
  if not ids:
@@ -1091,7 +1089,7 @@ class Env:
1091
1089
  if not self.db_name and (not self.uid or login == self.user.login) and self.session_info.get('db'):
1092
1090
  empty_db_key = (self.db_name, self.client._server)
1093
1091
  self.db_name = self.session_info['db']
1094
- for key in list(self._cache):
1092
+ for key in [*self._cache]:
1095
1093
  if key[1:] == empty_db_key:
1096
1094
  self._cache_set(key[0], self._cache[key])
1097
1095
  print(f'Session authenticated for {login!r}' if self.session_info['uid'] else 'Failed')
@@ -1306,7 +1304,7 @@ class Client:
1306
1304
  try:
1307
1305
  cls._saved_config[environment] = read_config(environment)
1308
1306
  except Exception:
1309
- envs = list(cls._saved_config)
1307
+ envs = [*cls._saved_config]
1310
1308
  if cls._config_file.exists():
1311
1309
  envs += [env for env in read_config() if env not in cls._saved_config]
1312
1310
  raise Error("No such environment.\nAvailable: " + ", ".join(envs))
@@ -1480,7 +1478,7 @@ class Client:
1480
1478
 
1481
1479
  @classmethod
1482
1480
  def _set_interactive(cls, global_vars={}):
1483
- global colorize, color_bold, color_comment
1481
+ global color_bold, color_comment, color_py, color_repr
1484
1482
  # Don't call multiple times
1485
1483
  del Client._set_interactive
1486
1484
  assert not cls._is_interactive()
@@ -1488,11 +1486,11 @@ class Client:
1488
1486
  global_vars[name] = globals()[name]
1489
1487
  try: # Python >= 3.14
1490
1488
  from _pyrepl.utils import disp_str, gen_colors, _colorize
1491
- colorize = lambda v: "".join(disp_str(v, colors=[*gen_colors(v)])[0])
1492
- colorize.__name__ = colorize.__qualname__ = 'colorize'
1493
- color_bold = colorize('def _').split()[1].replace('_', '{}').format
1494
- color_comment = colorize('#').replace('#', '{}').format
1495
- global_vars |= {'colorize': colorize, 'decolor': _colorize.decolor}
1489
+ color_py = color_repr = lambda v: "".join(disp_str(v, colors=[*gen_colors(v)])[0])
1490
+ color_py.__name__ = color_py.__qualname__ = 'color_python'
1491
+ color_bold = color_py('def _').split()[1].replace('_', '{}').format
1492
+ color_comment = color_py('#').replace('#', '{}').format
1493
+ global_vars |= {'color_py': color_py, 'decolor': _colorize.decolor}
1496
1494
  except ImportError:
1497
1495
  pass
1498
1496
  cls._globals = global_vars
@@ -1512,7 +1510,6 @@ class Client:
1512
1510
  and no country is set into the database.
1513
1511
  Login if successful.
1514
1512
  """
1515
- extra = (login, country_code) if login != ADMIN_USER or country_code else ()
1516
1513
  self.database.create(master_pwd=passwd, name=database, lang=lang,
1517
1514
  password=user_password, demo=demo, login=login,
1518
1515
  country_code=country_code, phone='')
@@ -1524,10 +1521,9 @@ class Client:
1524
1521
  The superadmin `passwd` and `database` are mandatory.
1525
1522
  Login if successful.
1526
1523
  """
1527
- extra = (neutralize_database,) if neutralize_database else ()
1524
+ extra = {"neutralize_database": neutralize_database} if neutralize_database else {}
1528
1525
  if extra and self.version_info < 16.0:
1529
1526
  raise Error("Argument 'neutralize_database' is not supported")
1530
- extra = {"neutralize_database": extra[0]} if extra else {}
1531
1527
  self.database.duplicate(master_pwd=passwd, name=self.env.db_name,
1532
1528
  new_name=database, **extra)
1533
1529
  # Copy the cache for authentication
@@ -1634,7 +1630,7 @@ class Model(BaseModel):
1634
1630
  methods = doc_dict.get('methods') or {}
1635
1631
  result = []
1636
1632
  for key, vals in methods.items():
1637
- arg_names = list(vals['parameters'])
1633
+ arg_names = [*vals['parameters']]
1638
1634
  if 'model' not in vals.get('api', ()):
1639
1635
  arg_names.insert(0, 'ids')
1640
1636
  result.append((key, arg_names))
@@ -1913,14 +1909,14 @@ class BaseRecord(BaseModel):
1913
1909
 
1914
1910
  def __sub__(self, other):
1915
1911
  self._check_model(other, '-')
1916
- other_ids = set(other.ids)
1912
+ other_ids = {*other.ids}
1917
1913
  ids = [idn for (id_, idn) in zip(self.ids, self._idnames)
1918
1914
  if id_ not in other_ids]
1919
1915
  return RecordList(self._model, ids)
1920
1916
 
1921
1917
  def __and__(self, other):
1922
1918
  self._check_model(other, '&')
1923
- other_ids = set(other.ids)
1919
+ other_ids = {*other.ids}
1924
1920
  self_set = self.union()
1925
1921
  ids = [idn for (id_, idn) in zip(self_set.ids, self_set._idnames)
1926
1922
  if id_ in other_ids]
@@ -1938,19 +1934,19 @@ class BaseRecord(BaseModel):
1938
1934
 
1939
1935
  def __lt__(self, other):
1940
1936
  self._check_model(other, '<')
1941
- return set(self.ids) < set(other.ids)
1937
+ return {*self.ids} < {*other.ids}
1942
1938
 
1943
1939
  def __le__(self, other):
1944
1940
  self._check_model(other, '<=')
1945
- return set(self.ids).issubset(other.ids)
1941
+ return {*self.ids}.issubset(other.ids)
1946
1942
 
1947
1943
  def __gt__(self, other):
1948
1944
  self._check_model(other, '>')
1949
- return set(self.ids) > set(other.ids)
1945
+ return {*self.ids} > {*other.ids}
1950
1946
 
1951
1947
  def __ge__(self, other):
1952
1948
  self._check_model(other, '>=')
1953
- return set(self.ids).issuperset(other.ids)
1949
+ return {*self.ids}.issuperset(other.ids)
1954
1950
 
1955
1951
  def __int__(self):
1956
1952
  return int(self.ensure_one().id)
@@ -2008,7 +2004,7 @@ class BaseRecord(BaseModel):
2008
2004
  raise TypeError(f"Mixing apples and oranges: {self} {oper} {other}")
2009
2005
 
2010
2006
  def _concat_ids(self, args):
2011
- ids = list(self._idnames)
2007
+ ids = [*self._idnames]
2012
2008
  for other in args:
2013
2009
  self._check_model(other, '+')
2014
2010
  ids.extend(other._idnames)
@@ -2026,7 +2022,7 @@ class BaseRecord(BaseModel):
2026
2022
  """
2027
2023
  ids = self._concat_ids(args)
2028
2024
  if len(ids) > 1:
2029
- seen = set()
2025
+ seen = {*()}
2030
2026
  uniqids = []
2031
2027
  for idn in ids:
2032
2028
  id_, name = idn if isinstance(idn, (list, tuple)) else (idn, None)
@@ -2269,7 +2265,7 @@ class Record(BaseRecord):
2269
2265
  else:
2270
2266
  idnames = [(arg, name)] = [arg]
2271
2267
  Ids, Id1 = self.env._class_ids
2272
- attrs = {'id': Id1(arg), 'ids': Ids([arg]), '_idnames': idnames, '_cached_keys': set()}
2268
+ attrs = {'id': Id1(arg), 'ids': Ids([arg]), '_idnames': idnames, '_cached_keys': {*()}}
2273
2269
  if name is not None:
2274
2270
  attrs['_Record__name'] = attrs['display_name'] = name
2275
2271
  self.__dict__.update(attrs)
@@ -2335,7 +2331,7 @@ class Record(BaseRecord):
2335
2331
  exist, only one of them is returned (randomly).
2336
2332
  """
2337
2333
  xml_ids = self._model._get_external_ids(self.ids)
2338
- return list(xml_ids)[0] if xml_ids else False
2334
+ return [*xml_ids][0] if xml_ids else False
2339
2335
 
2340
2336
  def _set_external_id(self, xml_id):
2341
2337
  """Set the External ID of this record."""
@@ -2407,7 +2403,7 @@ def _interact(global_vars, use_pprint=True, usage=USAGE):
2407
2403
  pass # IOError if file missing, or other error
2408
2404
 
2409
2405
  if use_pprint:
2410
- pp = lambda obj: print(colorize(pprint.pformat(obj, **PP_FORMAT)))
2406
+ pp = lambda obj: print(color_repr(pprint.pformat(obj, **PP_FORMAT)))
2411
2407
 
2412
2408
  def displayhook(value, _printer=pp, _builtins=builtins):
2413
2409
  # Pretty-format the output
@@ -2419,7 +2415,7 @@ def _interact(global_vars, use_pprint=True, usage=USAGE):
2419
2415
  def excepthook(exc_type, exc, tb):
2420
2416
  # Print readable errors
2421
2417
  msg = ''.join(format_exception(exc_type, exc, tb, chain=False, **exfmt))
2422
- print(colorize(msg.rstrip()))
2418
+ print(color_repr(msg.rstrip()))
2423
2419
  sys.excepthook = excepthook
2424
2420
 
2425
2421
  builtins.usage = type('Usage', (), {'__call__': lambda s: print(usage),
@@ -2484,7 +2480,7 @@ def main(interact=_interact):
2484
2480
  return
2485
2481
 
2486
2482
  global_vars = Client._set_interactive()
2487
- print(colorize(USAGE))
2483
+ print(color_repr(USAGE))
2488
2484
 
2489
2485
  connect_client(args)
2490
2486
  return interact(global_vars) if interact else global_vars
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env python
2
+ """Connect to an Odoo server."""
3
+ # Colorize command-line output
4
+ import builtins
5
+ import keyword
6
+ import io
7
+ import os
8
+ import re
9
+ import token as T
10
+ import tokenize
11
+ import unicodedata
12
+
13
+ import odooly
14
+
15
+ BUILTINS = {name for name in dir(builtins) if name[:1] != '_'}
16
+ T_STRING = {getattr(T, tk) for tk in dir(T) if 'STRING' in tk}
17
+ T_MIDDLE = {getattr(T, tk) for tk in dir(T) if 'STRING_MIDDLE' in tk}
18
+ KEYWORDS = {*keyword.kwlist}
19
+ SOFTKEYW = {*getattr(keyword, 'softkwlist', ())}
20
+ J_SPECIAL = {'false', 'null', 'true'}
21
+ CONSTANTS = {'False', 'None', 'True'}
22
+ DEF_CLASS = {'def', 'class'}
23
+
24
+
25
+ class THEME: # Default theme
26
+ keyword = '\x1b[1;34m'
27
+ builtin = '\x1b[36m'
28
+ comment = '\x1b[31m'
29
+ string = '\x1b[32m'
30
+ number = '\x1b[33m'
31
+ op = '\x1b[0m' # No color
32
+ definition = '\x1b[1m'
33
+ reset = '\x1b[0m'
34
+
35
+
36
+ def gen_colors(value, theme, keywords=KEYWORDS):
37
+ """Generate color tuples (start, end, color)."""
38
+ sio = io.StringIO(value)
39
+ line_offsets = [..., 0] + [sio.tell() for __ in sio]
40
+ sio.seek(0)
41
+
42
+ bracket_level = is_def_name = 0
43
+ yield None, 0, theme.reset
44
+ try:
45
+ for token in tokenize.generate_tokens(sio.readline):
46
+ if token.start == token.end:
47
+ continue
48
+ color, end_offset = None, 0
49
+ if token.type in T_STRING:
50
+ if token.type in T_MIDDLE and token.string[-1:] in '{}':
51
+ end_offset += 1
52
+ color = theme.string
53
+ elif token.type == T.COMMENT:
54
+ color = theme.comment
55
+ elif token.type == T.NUMBER:
56
+ color = theme.number
57
+ elif token.type == T.OP:
58
+ if token.string in '([{':
59
+ bracket_level += 1
60
+ elif token.string in ')]}':
61
+ bracket_level -= 1
62
+ color = theme.op
63
+ elif token.type == T.NAME:
64
+ if token.string in keywords:
65
+ is_def_name = token.string in DEF_CLASS
66
+ color = theme.keyword
67
+ elif keywords is not KEYWORDS:
68
+ continue
69
+ elif is_def_name:
70
+ is_def_name = False
71
+ color = theme.definition
72
+ elif token.string in SOFTKEYW and not bracket_level:
73
+ color = theme.keyword # soft_keyword
74
+ elif token.string in BUILTINS:
75
+ color = theme.builtin
76
+ if color is not None:
77
+ start = line_offsets[token.start[0]] + token.start[1]
78
+ end = line_offsets[token.end[0]] + token.end[1] + end_offset
79
+ yield start, end, color
80
+ except (SyntaxError, tokenize.TokenError):
81
+ pass
82
+ yield None, None, theme.reset
83
+
84
+
85
+ def _apply_colors(value, theme, keywords):
86
+ output = ''
87
+ if value:
88
+ __, end, c_reset = next(colors := gen_colors(value, theme=theme, keywords=keywords))
89
+
90
+ while (last_pos := end) is not None:
91
+ start, end, color = next(colors)
92
+ if start is None:
93
+ output += _escape(value[last_pos:])
94
+ else:
95
+ output += _escape(value[last_pos:start]) + color + _escape(value[start:end]) + c_reset
96
+ return output
97
+
98
+
99
+ def _escape(value):
100
+ if value.isascii():
101
+ return value
102
+ result = ''
103
+ for char in value:
104
+ if char > '\x7f' and unicodedata.category(char)[:1] == 'C':
105
+ char = rf'\u{ord(char):04x}'
106
+ result += char
107
+ return result
108
+
109
+
110
+ def color_python(value):
111
+ """Colorize Python syntax."""
112
+ return _apply_colors(value, THEME(), KEYWORDS)
113
+
114
+
115
+ def color_repr(value):
116
+ """Colorize string representation."""
117
+ return _apply_colors(value, THEME(), CONSTANTS)
118
+
119
+
120
+ def color_json(value):
121
+ """Colorize JSON representation."""
122
+ return _apply_colors(value, THEME(), J_SPECIAL)
123
+
124
+
125
+ def patch_colors(module):
126
+ """Set functions to color output."""
127
+ global THEME
128
+ try: # Python >= 3.14
129
+ from _pyrepl.utils import BUILTINS, THEME
130
+ BUILTINS |= {'Client', 'client', 'env', 'self'}
131
+ except ImportError:
132
+ pass
133
+
134
+ module.color_repr = color_repr
135
+ if module.color_py is not str: # Python >= 3.14
136
+ return {'color_repr': color_repr}
137
+ theme = THEME()
138
+ module.color_py = color_python
139
+ module.color_bold = f'{theme.definition}{{}}{theme.reset}'.format
140
+ module.color_comment = f'{theme.comment}{{}}{theme.reset}'.format
141
+ decolor = odooly.partial(re.compile(r'\x1b\[[;\d]+m').sub, '')
142
+ return {'color_py': color_python, 'color_repr': color_repr, 'decolor': decolor}
143
+
144
+
145
+ def main():
146
+ args = odooly.get_parser().parse_args()
147
+
148
+ if args.config:
149
+ odooly.Client._config_file = odooly.Path.cwd() / args.config
150
+ if args.list_env:
151
+ print('Available settings: ' + ' '.join(odooly.read_config()))
152
+ return
153
+
154
+ global_vars = odooly.Client._set_interactive()
155
+ if odooly.color_py is not str or (os.getenv('FORCE_COLOR') and not os.getenv('NO_COLOR')):
156
+ global_vars.update(patch_colors(odooly))
157
+
158
+ print(odooly.color_repr(odooly.USAGE))
159
+ odooly.connect_client(args)
160
+ odooly._interact(global_vars)
161
+
162
+
163
+ if __name__ == '__main__':
164
+ main()
@@ -1,131 +0,0 @@
1
- #!/usr/bin/env python
2
- """Connect to an Odoo server."""
3
- # Colorize command-line output for Python <= 3.13
4
- import builtins
5
- import keyword
6
- import io
7
- import os
8
- import pathlib
9
- import re
10
- import token as T
11
- import tokenize
12
- import unicodedata
13
-
14
- import odooly
15
-
16
- BUILTINS = {str(name) for name in dir(builtins) if name[:1] != '_'}
17
- T_STRING = {getattr(T, tk) for tk in dir(T) if 'STRING' in tk}
18
- T_MIDDLE = {getattr(T, tk) for tk in dir(T) if 'STRING_MIDDLE' in tk}
19
-
20
- issoftkeyword = getattr(keyword, 'issoftkeyword', lambda t: False)
21
-
22
-
23
- class THEME: # Default theme
24
- keyword = '\x1b[1;34m'
25
- builtin = '\x1b[36m'
26
- comment = '\x1b[31m'
27
- string = '\x1b[32m'
28
- number = '\x1b[33m'
29
- op = '\x1b[0m' # No color
30
- definition = '\x1b[1m'
31
- reset = '\x1b[0m'
32
-
33
-
34
- def gen_colors(value, theme=THEME()):
35
- """Generate color tuples (start, end, color, reset_color)."""
36
- sio = io.StringIO(value)
37
- line_lengths = [0] + [len(line) for line in sio.readlines()]
38
- for i in range(1, len(line_lengths)):
39
- line_lengths[i] += line_lengths[i-1]
40
-
41
- sio.seek(0)
42
- bracket_level = is_def_name = 0
43
- try:
44
- for token in tokenize.generate_tokens(sio.readline):
45
- if token.start == token.end:
46
- continue
47
- color, end_offset = None, -1
48
- if token.type in T_STRING:
49
- if token.type in T_MIDDLE and token.string.endswith(('{', '}')):
50
- end_offset += 1
51
- color = theme.string
52
- elif token.type == T.COMMENT:
53
- color = theme.comment
54
- elif token.type == T.NUMBER:
55
- color = theme.number
56
- elif token.type == T.OP:
57
- if token.string in '([{':
58
- bracket_level += 1
59
- elif token.string in ')]}':
60
- bracket_level -= 1
61
- color = theme.op
62
- elif token.type == T.NAME:
63
- if is_def_name:
64
- is_def_name = False
65
- color = theme.definition
66
- elif keyword.iskeyword(token.string):
67
- if token.string in ('def', 'class'):
68
- is_def_name = True
69
- color = theme.keyword
70
- elif issoftkeyword(token.string) and not bracket_level:
71
- color = theme.keyword # soft_keyword
72
- elif token.string in BUILTINS:
73
- color = theme.builtin
74
- if color is not None:
75
- start = line_lengths[token.start[0] - 1] + token.start[1]
76
- end = line_lengths[token.end[0] - 1] + token.end[1] + end_offset
77
- yield start, end, color, theme.reset
78
- except (SyntaxError, tokenize.TokenError):
79
- pass
80
- yield None, None, None, None
81
-
82
-
83
- def colorize(value):
84
- """Colorize Python syntax."""
85
- output = ''
86
- if value:
87
- start, end, color, c_reset = next(colors := gen_colors(value))
88
-
89
- for idx, char in enumerate(value):
90
- if start == idx:
91
- output += color
92
-
93
- if char > '\x7f' and unicodedata.category(char)[:1] == 'C':
94
- char = rf'\u{ord(char):04x}'
95
- output += char
96
-
97
- if end == idx:
98
- output += c_reset
99
- start, end, color, c_reset = next(colors)
100
- return output
101
-
102
-
103
- def patch_colors(module):
104
- """Set functions to color output."""
105
- module.colorize = colorize
106
- module.color_bold = f'{THEME.definition}{{}}{THEME.reset}'.format
107
- module.color_comment = f'{THEME.comment}{{}}{THEME.reset}'.format
108
- decolor = odooly.partial(re.compile(r'\x1b\[[;\d]+m').sub, '')
109
- return {'colorize': colorize, 'decolor': decolor}
110
-
111
-
112
- def main():
113
- args = odooly.get_parser().parse_args()
114
-
115
- if args.config:
116
- odooly.Client._config_file = odooly.Path.cwd() / args.config
117
- if args.list_env:
118
- print('Available settings: ' + ' '.join(odooly.read_config()))
119
- return
120
-
121
- global_vars = odooly.Client._set_interactive()
122
- if os.getenv('FORCE_COLOR') and not os.getenv('NO_COLOR') and odooly.colorize is str:
123
- global_vars.update(patch_colors(odooly)) # Python <= 3.13
124
-
125
- print(odooly.colorize(odooly.USAGE))
126
- odooly.connect_client(args)
127
- odooly._interact(global_vars)
128
-
129
-
130
- if __name__ == '__main__':
131
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes