From b16ca2449a5f15b81348618d8f271031a909f465 Mon Sep 17 00:00:00 2001 From: I335851 Date: Tue, 29 Sep 2026 13:41:32 +0200 Subject: [PATCH 1/5] chore: refactor _build_expression operator dispatch to match/case Replace the if/elif chain in _build_expression with a PEP 634 match/case statement, removing the need for pylint suppressions of too-many-return-statements and too-many-branches. Part of #302. --- pyodata/v2/service.py | 78 ++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 46 deletions(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 05449d4..ba5e96e 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -1285,58 +1285,44 @@ def _decode_expression(self, expr, val): def _combine_expressions(self, expressions): return ' and '.join(expressions) - # pylint: disable=too-many-return-statements, too-many-branches def _build_expression(self, field_name, operator, value): target_field = self.proprty_obj(field_name) if operator not in ['length', 'in', 'range']: value = target_field.to_literal(value) - if operator == 'lt': - return f'{field_name} lt {value}' - - if operator == 'lte': - return f'{field_name} le {value}' - - if operator == 'gte': - return f'{field_name} ge {value}' - - if operator == 'gt': - return f'{field_name} gt {value}' - - if operator == 'startswith': - return f'startswith({field_name}, {value}) eq true' - - if operator == 'endswith': - return f'endswith({field_name}, {value}) eq true' - - if operator == 'length': - value = int(value) - return f'length({field_name}) eq {value}' - - if operator in ['contains']: - return f'substringof({value}, {field_name}) eq true' - - if operator == 'range': - if not isinstance(value, tuple | list): - raise TypeError(f'Range must be tuple or list not {type(value)}') - - if len(value) != 2: - raise ValueError('Only two items can be passed in a range.') - - low_bound = target_field.to_literal(value[0]) - high_bound = target_field.to_literal(value[1]) - - return f'{field_name} gte {low_bound} and {field_name} lte {high_bound}' - - if operator == 'in': - literal_values = (f'{field_name} eq {target_field.to_literal(item)}' for item in value) - return ' or '.join(literal_values) - - if operator == 'eq': - return f'{field_name} eq {value}' - - raise ValueError(f'Invalid expression {operator}') + match operator: + case 'lt': + return f'{field_name} lt {value}' + case 'lte': + return f'{field_name} le {value}' + case 'gte': + return f'{field_name} ge {value}' + case 'gt': + return f'{field_name} gt {value}' + case 'startswith': + return f'startswith({field_name}, {value}) eq true' + case 'endswith': + return f'endswith({field_name}, {value}) eq true' + case 'length': + return f'length({field_name}) eq {int(value)}' + case 'contains': + return f'substringof({value}, {field_name}) eq true' + case 'range': + if not isinstance(value, tuple | list): + raise TypeError(f'Range must be tuple or list not {type(value)}') + if len(value) != 2: + raise ValueError('Only two items can be passed in a range.') + low_bound = target_field.to_literal(value[0]) + high_bound = target_field.to_literal(value[1]) + return f'{field_name} gte {low_bound} and {field_name} lte {high_bound}' + case 'in': + literal_values = (f'{field_name} eq {target_field.to_literal(item)}' for item in value) + return ' or '.join(literal_values) + case 'eq': + return f'{field_name} eq {value}' + case _: + raise ValueError(f'Invalid expression {operator}') def __str__(self): expressions = self._process_expressions() From d62de72bc7a3f8f584a52dde6a89846c6e4ff191 Mon Sep 17 00:00:00 2001 From: I335851 Date: Tue, 29 Sep 2026 13:53:22 +0200 Subject: [PATCH 2/5] chore: bump pylint to 2.11.0 for match/case AST support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pylint 2.11.0 is the first release bundling astroid 2.9.x, which added visit_match — required to lint Python 3.10 match/case statements without crashing. This is intentionally minimal pylint bump, even that pylint is currently 2 major versions above - there are too much new failures when trying the latest linter. Part of #302. --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index fee63f9..d713fe0 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,7 +6,7 @@ httpx>=0.23.0 respx>=0.19.2 setuptools>=38.2.4 setuptools-scm>=1.15.6 -pylint==2.8.3 +pylint==2.11.0 pytest-cov>=3.0.0 flake8==3.8.4 bandit==1.7.5 From ee613e0a9f7c5d1bf32d17b9c615f6ecf865c3e9 Mon Sep 17 00:00:00 2001 From: I335851 Date: Tue, 29 Sep 2026 14:15:53 +0200 Subject: [PATCH 3/5] chore: suppress new pylint 2.11.0 warnings to restore 10/10 score Disable C0209, R1735, R1734, and W0602 globally in .pylintrc, and add an inline too-many-return-statements suppression to _build_expression where match/case inherently exceeds the return-count limit. Fixes for those will be tracked in a follow-up PR. Part of #302. --- .pylintrc | 6 +++++- pyodata/v2/service.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 7dc311f..325c27b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,7 +65,11 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=locally-disabled, super-with-arguments, raise-missing-from +disable=locally-disabled, super-with-arguments, raise-missing-from, + consider-using-f-string, + use-dict-literal, + use-list-literal, + global-variable-not-assigned [REPORTS] diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index ba5e96e..415cd84 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -1285,6 +1285,7 @@ def _decode_expression(self, expr, val): def _combine_expressions(self, expressions): return ' and '.join(expressions) + # pylint: disable=too-many-return-statements def _build_expression(self, field_name, operator, value): target_field = self.proprty_obj(field_name) From b4f4923f51223df48937a2c2c65537f9d146373d Mon Sep 17 00:00:00 2001 From: I335851 Date: Tue, 29 Sep 2026 15:32:29 +0200 Subject: [PATCH 4/5] chore: bump pylint to 2.15.0 to fix Python 3.11 compatibility pylint 2.11.0 depends on wrapt<1.14 which fails to build on Python 3.11 because inspect.formatargspec was removed. pylint 2.15.0 requires wrapt>=1.14 which supports Python 3.11. Part of #302. --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index d713fe0..f40feda 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,7 +6,7 @@ httpx>=0.23.0 respx>=0.19.2 setuptools>=38.2.4 setuptools-scm>=1.15.6 -pylint==2.11.0 +pylint==2.15.0 pytest-cov>=3.0.0 flake8==3.8.4 bandit==1.7.5 From 10836beaf3b46c42ab4ffd357088eea88a306002 Mon Sep 17 00:00:00 2001 From: I335851 Date: Tue, 29 Sep 2026 15:46:20 +0200 Subject: [PATCH 5/5] chore: suppress new pylint 2.15.0 warnings to restore 10/10 score Remove obsolete .pylintrc options dropped in pylint 2.14/2.15 (optimize-ast, files-output, no-space-check, *-name-hint keys) and globally suppress unrecognized-option / useless-option-value. Remove 32 inline no-self-use suppressions from model.py and service.py that now trigger R0022 because no-self-use was moved to an optional extension in pylint 2.14. Part of #302. --- .pylintrc | 55 +++---------------------------------------- pyodata/v2/model.py | 19 --------------- pyodata/v2/service.py | 13 ---------- 3 files changed, 3 insertions(+), 84 deletions(-) diff --git a/.pylintrc b/.pylintrc index 325c27b..546127b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -34,15 +34,6 @@ unsafe-load-any-extension=no # run arbitrary code extension-pkg-whitelist=lxml -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. This option is deprecated -# and it will be removed in Pylint 2.0. -optimize-ast=no - [MESSAGES CONTROL] @@ -69,7 +60,9 @@ disable=locally-disabled, super-with-arguments, raise-missing-from, consider-using-f-string, use-dict-literal, use-list-literal, - global-variable-not-assigned + global-variable-not-assigned, + unrecognized-option, + useless-option-value [REPORTS] @@ -79,12 +72,6 @@ disable=locally-disabled, super-with-arguments, raise-missing-from, # mypackage.mymodule.MyReporterClass. output-format=text -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". This option is deprecated -# and it will be removed in Pylint 2.0. -files-output=no - # Tells whether to display a full report or only the messages reports=no @@ -122,63 +109,33 @@ property-classes=abc.abstractproperty # Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_-]*)|([A-Z][a-zA-Z0-9]+))$ -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]{2,42}$ -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,42}$ - # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=^_ @@ -206,12 +163,6 @@ ignore-long-lines=^\s*(# )??$ # else. single-line-if-stmt=no -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - # Maximum number of lines in a module max-module-lines=1000 diff --git a/pyodata/v2/model.py b/pyodata/v2/model.py index 9d6b912..7c53df0 100644 --- a/pyodata/v2/model.py +++ b/pyodata/v2/model.py @@ -282,7 +282,6 @@ class EdmStructTypeSerializer: @staticmethod def to_literal(edm_type, value): - # pylint: disable=no-self-use if not edm_type: raise PyODataException(f'Cannot encode value {value} without complex type information') @@ -296,7 +295,6 @@ def to_literal(edm_type, value): @staticmethod def from_json(edm_type, value): - # pylint: disable=no-self-use if not edm_type: raise PyODataException(f'Cannot decode value {value} without complex type information') @@ -310,7 +308,6 @@ def from_json(edm_type, value): @staticmethod def from_literal(edm_type, value): - # pylint: disable=no-self-use if not edm_type: raise PyODataException(f'Cannot decode value {value} without complex type information') @@ -328,11 +325,9 @@ class TypTraits: def __repr__(self): return self.__class__.__name__ - # pylint: disable=no-self-use def to_literal(self, value): return value - # pylint: disable=no-self-use def from_json(self, value): return value @@ -568,11 +563,9 @@ def from_literal(self, value): class EdmStringTypTraits(TypTraits): """Edm.String traits""" - # pylint: disable=no-self-use def to_literal(self, value): return '\'%s\'' % (str(value).replace("'", "''")) - # pylint: disable=no-self-use def from_json(self, value): return value @@ -585,11 +578,9 @@ def from_literal(self, value): class EdmBooleanTypTraits(TypTraits): """Edm.Boolean traits""" - # pylint: disable=no-self-use def to_literal(self, value): return 'true' if value else 'false' - # pylint: disable=no-self-use def from_json(self, value): return value @@ -600,11 +591,9 @@ def from_literal(self, value): class EdmIntTypTraits(TypTraits): """All Edm Integer traits""" - # pylint: disable=no-self-use def to_literal(self, value): return '%d' % (value) - # pylint: disable=no-self-use def from_json(self, value): return int(value) @@ -615,11 +604,9 @@ def from_literal(self, value): class EdmLongIntTypTraits(TypTraits): """All Edm Integer for big numbers traits""" - # pylint: disable=no-self-use def to_literal(self, value): return '%dL' % (value) - # pylint: disable=no-self-use def from_json(self, value): if value[-1] == 'L': return int(value[:-1]) @@ -655,14 +642,12 @@ def edm_double(): def edm_single(): return EdmFPNumTypTraits(7, 'f', '{:f}') - # pylint: disable=no-self-use def to_literal(self, value): return self.conversion.format(value) def to_json(self, value): return self.to_literal(value) - # pylint: disable=no-self-use def from_json(self, value): if not isinstance(value, str) or value[-1] != self.suffix: return float(value) @@ -680,11 +665,9 @@ def __init__(self, edm_type=None): super().__init__() self._edm_type = edm_type - # pylint: disable=no-self-use def to_literal(self, value): return EdmStructTypeSerializer.to_literal(self._edm_type, value) - # pylint: disable=no-self-use def from_json(self, value): return EdmStructTypeSerializer.from_json(self._edm_type, value) @@ -761,14 +744,12 @@ def item_type(self): def traits(self): return self - # pylint: disable=no-self-use def to_literal(self, value): if not isinstance(value, list): raise PyODataException(f'Bad format: invalid list value {value}') return [self._item_type.traits.to_literal(v) for v in value] - # pylint: disable=no-self-use def from_json(self, value): if not isinstance(value, list): raise PyODataException(f'Bad format: invalid list value {value}') diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 415cd84..ec58e20 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -250,27 +250,22 @@ def handler(self): def get_path(self): """Get path of the HTTP request""" - # pylint: disable=no-self-use return '' def get_query_params(self): """Get query params""" - # pylint: disable=no-self-use return dict(self._customs) def get_method(self): """Get HTTP method""" - # pylint: disable=no-self-use return 'GET' def get_body(self): """Get HTTP body or None if not applicable""" - # pylint: disable=no-self-use return None def get_default_headers(self): """Get dict of Child specific HTTP headers""" - # pylint: disable=no-self-use return dict() def get_headers(self): @@ -500,7 +495,6 @@ def get_path(self): return self._last_segment def get_method(self): - # pylint: disable=no-self-use return 'POST' def _get_body(self): @@ -583,7 +577,6 @@ def get_encode_path(self): return self._encode_path def get_method(self): - # pylint: disable=no-self-use return 'DELETE' @@ -624,11 +617,9 @@ def get_path(self): return self._entity_set.name + self._entity_key.to_key_string() def get_method(self): - # pylint: disable=no-self-use return self._method def get_body(self): - # pylint: disable=no-self-use body = {} for key, val in self._values.items(): body[key] = val @@ -1281,7 +1272,6 @@ def _decode_expression(self, expr, val): return expression - # pylint: disable=no-self-use def _combine_expressions(self, expressions): return ' and '.join(expressions) @@ -1926,7 +1916,6 @@ def get_boundary(self): return self.id def get_default_headers(self): - # pylint: disable=no-self-use return {'Content-Type': f'multipart/mixed;boundary={self.get_boundary()}'} def get_body(self): @@ -1961,11 +1950,9 @@ def get_boundary(self): return 'batch_' + self.id def get_path(self): - # pylint: disable=no-self-use return '$batch' def get_method(self): - # pylint: disable=no-self-use return 'POST'