From bc653da9afec59bbb9c57230ffa2a12374f0346c Mon Sep 17 00:00:00 2001 From: Mahmoud Hashemi Date: Sat, 2 Nov 2024 16:09:15 -0700 Subject: [PATCH] Add Python 3.12 (#285) * fix packaging tox job * add py312 to tox, setup.py, and github actions, plus refresh requirements * upgrade syntax to py37+ and autoconvert some f-strings. add tox syntax-upgrade task. --- .github/workflows/tests.yaml | 30 +++++------ docs/conf.py | 2 +- glom/__init__.py | 1 - glom/__main__.py | 1 - glom/cli.py | 17 +++--- glom/core.py | 95 +++++++++++++++++----------------- glom/grouping.py | 23 ++++---- glom/matching.py | 62 +++++++++++----------- glom/mutation.py | 14 ++--- glom/reduction.py | 13 +++-- glom/streaming.py | 10 ++-- glom/test/perf_report.py | 2 +- glom/test/test_basic.py | 15 +++--- glom/test/test_check.py | 3 +- glom/test/test_cli.py | 3 -- glom/test/test_error.py | 35 ++++++------- glom/test/test_grouping.py | 2 - glom/test/test_match.py | 5 +- glom/test/test_mutation.py | 12 ++--- glom/test/test_path_and_t.py | 3 +- glom/test/test_reduction.py | 1 - glom/test/test_scope_vars.py | 5 +- glom/test/test_snippets.py | 2 +- glom/test/test_spec.py | 1 - glom/test/test_streaming.py | 1 - glom/test/test_target_types.py | 13 ++--- glom/test/test_tutorial.py | 1 - glom/tutorial.py | 6 +-- requirements.txt | 93 ++++++++++++++++++++++++--------- setup.py | 3 ++ tox.ini | 13 ++++- 31 files changed, 260 insertions(+), 227 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f9911359..c90bfdc3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -2,14 +2,14 @@ name: Tests on: push: paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' + - "docs/**" + - "*.md" + - "*.rst" pull_request: paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' + - "docs/**" + - "*.md" + - "*.rst" jobs: tests: name: ${{ matrix.name }} @@ -18,14 +18,15 @@ jobs: fail-fast: false matrix: include: - - {name: Linux, python: '3.11', os: ubuntu-latest, tox: py311} - - {name: Windows, python: '3.11', os: windows-latest, tox: py311} - - {name: Mac, python: '3.11', os: macos-latest, tox: py311} - - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} - - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - - {name: 'PyPy3', python: 'pypy-3.9', os: ubuntu-latest, tox: pypy3} + - { name: Linux, python: "3.12", os: ubuntu-latest, tox: py312 } + - { name: Windows, python: "3.12", os: windows-latest, tox: py312 } + - { name: Mac, python: "3.12", os: macos-latest, tox: py312 } + - { name: "3.11", python: "3.11", os: ubuntu-latest, tox: py311 } + - { name: "3.10", python: "3.10", os: ubuntu-latest, tox: py310 } + - { name: "3.9", python: "3.9", os: ubuntu-latest, tox: py39 } + - { name: "3.8", python: "3.8", os: ubuntu-latest, tox: py38 } + - { name: "3.7", python: "3.7", os: ubuntu-latest, tox: py37 } + - { name: "PyPy3", python: "pypy-3.9", os: ubuntu-latest, tox: pypy3 } steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 @@ -52,4 +53,3 @@ jobs: fail_ci_if_error: true files: ./.tox/coverage.xml token: ${{ secrets.CODECOV_TOKEN }} - diff --git a/docs/conf.py b/docs/conf.py index 50c98b9b..c367f256 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,7 +32,7 @@ # -- Project information ----------------------------------------------------- project = u'glom' -copyright = u'2023, Mahmoud Hashemi' +copyright = u'2024, Mahmoud Hashemi' author = u'Mahmoud Hashemi' # The short X.Y version diff --git a/glom/__init__.py b/glom/__init__.py index 77bd1edb..1fb4035d 100644 --- a/glom/__init__.py +++ b/glom/__init__.py @@ -1,4 +1,3 @@ - from glom.core import (glom, Fill, Auto, diff --git a/glom/__main__.py b/glom/__main__.py index a9c2d646..9935c57e 100644 --- a/glom/__main__.py +++ b/glom/__main__.py @@ -1,4 +1,3 @@ - from glom.cli import console_main if __name__ == '__main__': diff --git a/glom/cli.py b/glom/cli.py index 21488f56..29870642 100644 --- a/glom/cli.py +++ b/glom/cli.py @@ -28,7 +28,6 @@ """ -from __future__ import print_function import os import ast @@ -65,7 +64,7 @@ def glom_cli(target, spec, indent, debug, inspect, scalar): try: result = glom.glom(target, spec) except GlomError as ge: - print('%s: %s' % (ge.__class__.__name__, ge)) + print(f'{ge.__class__.__name__}: {ge}') return 1 if not indent: @@ -177,10 +176,10 @@ def mw_get_target(next_, posargs_, target_file, target_format, spec_file, spec_f raise UsageError('expected spec file or spec argument, not both') elif spec_file: try: - with open(spec_file, 'r') as f: + with open(spec_file) as f: spec_text = f.read() - except IOError as ose: - raise UsageError('could not read spec file %r, got: %s' % (spec_file, ose)) + except OSError as ose: + raise UsageError(f'could not read spec file {spec_file!r}, got: {ose}') if not spec_text: spec = Path() @@ -202,9 +201,9 @@ def mw_get_target(next_, posargs_, target_file, target_format, spec_file, spec_f target_text = sys.stdin.read() elif target_file: try: - target_text = open(target_file, 'r').read() - except IOError as ose: - raise UsageError('could not read target file %r, got: %s' % (target_file, ose)) + target_text = open(target_file).read() + except OSError as ose: + raise UsageError(f'could not read target file {target_file!r}, got: {ose}') elif not target_text and not isatty(sys.stdin): target_text = sys.stdin.read() @@ -225,7 +224,7 @@ def _from_glom_import_star(): def _eval_python_full_spec(py_text): name = '__cli_glom_spec__' - code_str = '%s = %s' % (name, py_text) + code_str = f'{name} = {py_text}' env = _from_glom_import_star() spec = _compile_code(code_str, name=name, env=env) return spec diff --git a/glom/core.py b/glom/core.py index 1f8a91ee..cee71a30 100644 --- a/glom/core.py +++ b/glom/core.py @@ -17,7 +17,6 @@ """ -from __future__ import print_function import os import sys @@ -137,7 +136,7 @@ def wrap(cls, exc): # defined in pure-python as well as C exc_type = type(exc) bases = (GlomError,) if issubclass(GlomError, exc_type) else (exc_type, GlomError) - exc_wrapper_type = type("GlomError.wrap({})".format(exc_type.__name__), bases, {}) + exc_wrapper_type = type(f"GlomError.wrap({exc_type.__name__})", bases, {}) try: wrapper = exc_wrapper_type(*exc.args) wrapper.__wrapped = exc @@ -183,7 +182,7 @@ def __str__(self): try: exc_get_message = self.get_message except AttributeError: - exc_get_message = super(GlomError, self).__str__ + exc_get_message = super().__str__ return exc_get_message() @@ -352,7 +351,7 @@ def get_message(self): def __repr__(self): cn = self.__class__.__name__ - return '%s(%r, %r, %r)' % (cn, self.exc, self.path, self.part_idx) + return f'{cn}({self.exc!r}, {self.path!r}, {self.part_idx!r})' class PathAssignError(GlomError): @@ -382,7 +381,7 @@ def get_message(self): def __repr__(self): cn = self.__class__.__name__ - return '%s(%r, %r, %r)' % (cn, self.exc, self.path, self.dest_name) + return f'{cn}({self.exc!r}, {self.path!r}, {self.dest_name!r})' class CoalesceError(GlomError): @@ -423,7 +422,7 @@ def __init__(self, coal_obj, skipped, path): def __repr__(self): cn = self.__class__.__name__ - return '%s(%r, %r, %r)' % (cn, self.coal_obj, self.skipped, self.path) + return f'{cn}({self.coal_obj!r}, {self.skipped!r}, {self.path!r})' def get_message(self): missed_specs = tuple(self.coal_obj.subspecs) @@ -434,11 +433,11 @@ def get_message(self): msg = ('no valid values found. Tried %r and got (%s)' % (missed_specs, ', '.join(skipped_vals))) if self.coal_obj.skip is not _MISSING: - msg += ', skip set to %r' % (self.coal_obj.skip,) + msg += f', skip set to {self.coal_obj.skip!r}' if self.coal_obj.skip_exc is not GlomError: - msg += ', skip_exc set to %r' % (self.coal_obj.skip_exc,) + msg += f', skip_exc set to {self.coal_obj.skip_exc!r}' if self.path is not None: - msg += ' (at path %r)' % (self.path,) + msg += f' (at path {self.path!r})' return msg @@ -475,7 +474,7 @@ def __init__(self, op, target_type, type_map, path): self.target_type = target_type self.type_map = type_map self.path = path - super(UnregisteredTarget, self).__init__(op, target_type, type_map, path) + super().__init__(op, target_type, type_map, path) def __repr__(self): cn = self.__class__.__name__ @@ -493,7 +492,7 @@ def get_message(self): msg = ("target type %r not registered for '%s', expected one of" " registered types: %s" % (self.target_type.__name__, self.op, reg_types_str)) if self.path: - msg += ' (at %r)' % (self.path,) + msg += f' (at {self.path!r})' return msg @@ -503,8 +502,8 @@ def get_message(self): __builtins__ = __builtins__.__dict__ -_BUILTIN_ID_NAME_MAP = dict([(id(v), k) - for k, v in __builtins__.items()]) +_BUILTIN_ID_NAME_MAP = {id(v): k + for k, v in __builtins__.items()} class _BBRepr(Repr): @@ -537,7 +536,7 @@ class _BBReprFormatter(string.Formatter): def convert_field(self, value, conversion): if conversion == 'r': return bbrepr(value).replace("\\'", "'") - return super(_BBReprFormatter, self).convert_field(value, conversion) + return super().convert_field(value, conversion) bbformat = _BBReprFormatter().format @@ -565,17 +564,17 @@ def format_invocation(name='', args=(), kwargs=None, **kw): kwarg_items = [(k, kwargs[k]) for k in sorted(kwargs)] else: kwarg_items = kwargs - kw_text = ', '.join(['%s=%s' % (k, _repr(v)) for k, v in kwarg_items]) + kw_text = ', '.join([f'{k}={_repr(v)}' for k, v in kwarg_items]) all_args_text = a_text if all_args_text and kw_text: all_args_text += ', ' all_args_text += kw_text - return '%s(%s)' % (name, all_args_text) + return f'{name}({all_args_text})' -class Path(object): +class Path: """Path objects specify explicit paths when the default ``'a.b.c'``-style general access syntax won't work or isn't desirable. Use this to wrap ints, datetimes, and other valid @@ -779,7 +778,7 @@ def _format_path(t_path): return _format_t(cur_t_path) -class Spec(object): +class Spec: """Spec objects serve three purposes, here they are, roughly ordered by utility: @@ -820,11 +819,11 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ if self.scope: - return '%s(%s, scope=%r)' % (cn, bbrepr(self.spec), self.scope) - return '%s(%s)' % (cn, bbrepr(self.spec)) + return f'{cn}({bbrepr(self.spec)}, scope={self.scope!r})' + return f'{cn}({bbrepr(self.spec)})' -class Coalesce(object): +class Coalesce: """Coalesce objects specify fallback behavior for a list of subspecs. @@ -915,7 +914,7 @@ def __init__(self, *subspecs, **kwargs): self.skip_func = lambda v: v == self.skip self.skip_exc = kwargs.pop('skip_exc', GlomError) if kwargs: - raise TypeError('unexpected keyword args: %r' % (sorted(kwargs.keys()),)) + raise TypeError(f'unexpected keyword args: {sorted(kwargs.keys())!r}') def glomit(self, target, scope): skipped = [] @@ -942,7 +941,7 @@ def __repr__(self): return format_invocation(cn, self.subspecs, self._orig_kwargs, repr=bbrepr) -class Inspect(object): +class Inspect: """The :class:`~glom.Inspect` specifier type provides a way to get visibility into glom's evaluation of a specification, enabling debugging of those tricky problems that may arise with unexpected @@ -1041,7 +1040,7 @@ def _trace(self, target, spec, scope): return ret -class Call(object): +class Call: """:class:`Call` specifies when a target should be passed to a function, *func*. @@ -1097,7 +1096,7 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ - return '%s(%s, args=%r, kwargs=%r)' % (cn, bbrepr(self.func), self.args, self.kwargs) + return f'{cn}({bbrepr(self.func)}, args={self.args!r}, kwargs={self.kwargs!r})' def _is_spec(obj, strict=False): @@ -1110,7 +1109,7 @@ def _is_spec(obj, strict=False): return _has_callable_glomit(obj) # pragma: no cover -class Invoke(object): +class Invoke: """Specifier type designed for easy invocation of callables from glom. Args: @@ -1322,7 +1321,7 @@ def glomit(self, target, scope): return func(*all_args, **all_kwargs) -class Ref(object): +class Ref: """Name a part of a spec and refer to it elsewhere in the same spec, useful for trees and other self-similar data structures. @@ -1351,7 +1350,7 @@ def __repr__(self): return "Ref(" + args + ")" -class TType(object): +class TType: """``T``, short for "target". A singleton object that enables object-oriented expression of a glom specification. @@ -1440,7 +1439,7 @@ def __getitem__(self, item): def __call__(self, *args, **kwargs): if self is S: if args: - raise TypeError('S() takes no positional arguments, got: %r' % (args,)) + raise TypeError(f'S() takes no positional arguments, got: {args!r}') if not kwargs: raise TypeError('S() expected at least one kwarg, got none') # TODO: typecheck kwarg vals? @@ -1734,7 +1733,7 @@ def _format_t(path, root=T): index = ", ".join([_format_slice(x) for x in arg]) else: index = _format_slice(arg) - prepr.append("[%s]" % (index,)) + prepr.append(f"[{index}]") elif op == '(': args, kwargs = arg prepr.append(format_invocation(args=args, kwargs=kwargs, repr=bbrepr)) @@ -1760,7 +1759,7 @@ def _format_t(path, root=T): return "".join(prepr) -class Val(object): +class Val: """Val objects are specs which evaluate to the wrapped *value*. >>> target = {'a': {'b': 'c'}} @@ -1789,13 +1788,13 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ - return '%s(%s)' % (cn, bbrepr(self.value)) + return f'{cn}({bbrepr(self.value)})' Literal = Val # backwards compat for pre-20.7.0 -class ScopeVars(object): +class ScopeVars: """This is the runtime partner of :class:`Vars` -- this is what actually lives in the scope and stores runtime values. @@ -1813,10 +1812,10 @@ def __iter__(self): return iter(self.__dict__.items()) def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, bbrepr(self.__dict__)) + return f"{self.__class__.__name__}({bbrepr(self.__dict__)})" -class Vars(object): +class Vars: """ :class:`Vars` is a helper that can be used with **S** in order to store shared mutable state. @@ -1843,7 +1842,7 @@ def __repr__(self): return ret -class Let(object): +class Let: """ Deprecated, kept for backwards compat. Use S(x='y') instead. @@ -1868,7 +1867,7 @@ def __repr__(self): return format_invocation(cn, kwargs=self._binding, repr=bbrepr) -class Auto(object): +class Auto: """ Switch to Auto mode (the default) @@ -1886,7 +1885,7 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ rpr = '' if self.spec is None else bbrepr(self.spec) - return '%s(%s)' % (cn, rpr) + return f'{cn}({rpr})' class _AbstractIterable(_AbstractIterableBase): @@ -1967,7 +1966,7 @@ def _handle_tuple(target, spec, scope): return res -class Pipe(object): +class Pipe: """Evaluate specs one after the other, passing the result of the previous evaluation in as the target of the next spec: @@ -1987,7 +1986,7 @@ def __repr__(self): return self.__class__.__name__ + bbrepr(self.steps) -class TargetRegistry(object): +class TargetRegistry: ''' responsible for registration of target types for iteration and attribute walking @@ -2094,7 +2093,7 @@ def _register_fuzzy_type(self, op, new_type, _type_tree=None): def register(self, target_type, **kwargs): if not isinstance(target_type, type): - raise TypeError('register expected a type, not an instance: %r' % (target_type,)) + raise TypeError(f'register expected a type, not an instance: {target_type!r}') exact = kwargs.pop('exact', None) new_op_map = dict(kwargs) @@ -2140,11 +2139,11 @@ def register_op(self, op_name, auto_func=None, exact=False): extensions. """ if not isinstance(op_name, basestring): - raise TypeError('expected op_name to be a text name, not: %r' % (op_name,)) + raise TypeError(f'expected op_name to be a text name, not: {op_name!r}') if auto_func is None: auto_func = lambda t: False elif not callable(auto_func): - raise TypeError('expected auto_func to be callable, not: %r' % (auto_func,)) + raise TypeError(f'expected auto_func to be callable, not: {auto_func!r}') # determine support for any previously known types known_types = set(sum([list(m.keys()) for m @@ -2320,7 +2319,7 @@ def chain_child(scope): return nxt_in_chain -unbound_methods = set([type(str.__len__)]) #, type(Ref.glomit)]) +unbound_methods = {type(str.__len__)} #, type(Ref.glomit)]) def _has_callable_glomit(obj): @@ -2438,7 +2437,7 @@ def register_op(op_name, **kwargs): return -class Glommer(object): +class Glommer: """The :class:`Glommer` type mostly serves to encapsulate type registration context so that advanced uses of glom don't need to worry about stepping on each other. @@ -2506,7 +2505,7 @@ def glom(self, target, spec, **kwargs): return glom(target, spec, scope=self.scope, **kwargs) -class Fill(object): +class Fill: """A specifier type which switches to glom into "fill-mode". For the spec contained within the Fill, glom will only interpret explicit specifier types (including T objects). Whereas the default mode @@ -2538,7 +2537,7 @@ def fill(self, target): def __repr__(self): cn = self.__class__.__name__ rpr = '' if self.spec is None else bbrepr(self.spec) - return '%s(%s)' % (cn, rpr) + return f'{cn}({rpr})' def FILL(target, spec, scope): @@ -2557,7 +2556,7 @@ def FILL(target, spec, scope): return spec(target) return spec -class _ArgValuator(object): +class _ArgValuator: def __init__(self): self.cache = {} diff --git a/glom/grouping.py b/glom/grouping.py index 650dd606..122bd086 100644 --- a/glom/grouping.py +++ b/glom/grouping.py @@ -1,7 +1,6 @@ """ Group mode """ -from __future__ import division import random @@ -41,7 +40,7 @@ def target_iter(target, scope): return iterator -class Group(object): +class Group: """supports nesting grouping operations -- think of a glom-style recursive boltons.iterutils.bucketize @@ -93,7 +92,7 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ - return '%s(%r)' % (cn, self.spec) + return f'{cn}({self.spec!r})' def GROUP(target, spec, scope): @@ -153,10 +152,10 @@ def GROUP(target, spec, scope): if result is not SKIP: acc.append(result) return acc - raise ValueError("{} not a valid spec type for Group mode".format(_spec_type)) # pragma: no cover + raise ValueError(f"{_spec_type} not a valid spec type for Group mode") # pragma: no cover -class First(object): +class First: """ holds onto the first value @@ -175,7 +174,7 @@ def __repr__(self): return '%s()' % self.__class__.__name__ -class Avg(object): +class Avg: """ takes the numerical average of all values; raises exception on non-numeric value @@ -199,7 +198,7 @@ def __repr__(self): return '%s()' % self.__class__.__name__ -class Max(object): +class Max: """ takes the maximum of all values; raises exception on values that are not comparable @@ -218,7 +217,7 @@ def __repr__(self): return '%s()' % self.__class__.__name__ -class Min(object): +class Min: """ takes the minimum of all values; raises exception on values that are not comparable @@ -237,7 +236,7 @@ def __repr__(self): return '%s()' % self.__class__.__name__ -class Sample(object): +class Sample: """takes a random sample of the values >>> glom([1, 2, 3], Group(Sample(2))) # doctest: +SKIP @@ -271,11 +270,11 @@ def agg(self, target, tree): return sample def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self.size) + return f'{self.__class__.__name__}({self.size!r})' -class Limit(object): +class Limit: """ Limits the number of values passed to sub-accumulator @@ -314,4 +313,4 @@ def glomit(self, target, scope): return scope[glom](target, self.subspec, scope) def __repr__(self): - return '%s(%r, %r)' % (self.__class__.__name__, self.n, self.subspec) + return f'{self.__class__.__name__}({self.n!r}, {self.subspec!r})' diff --git a/glom/matching.py b/glom/matching.py index 0a42719a..798ac402 100644 --- a/glom/matching.py +++ b/glom/matching.py @@ -35,7 +35,7 @@ class MatchError(GlomError): """ def __init__(self, fmt, *args): - super(MatchError, self).__init__(fmt, *args) + super().__init__(fmt, *args) def get_message(self): fmt, args = self.args[0], self.args[1:] @@ -60,7 +60,7 @@ class TypeMatchError(MatchError, TypeError): """ def __init__(self, actual, expected): - super(TypeMatchError, self).__init__( + super().__init__( "expected type {0.__name__}, not {1.__name__}", expected, actual) def __copy__(self): @@ -69,7 +69,7 @@ def __copy__(self): return TypeMatchError(self.args[2], self.args[1]) -class Match(object): +class Match: """glom's ``Match`` specifier type enables a new mode of glom usage: pattern matching. In particular, this mode has been designed for nested data validation. @@ -186,24 +186,24 @@ def matches(self, target): return True def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, bbrepr(self.spec)) + return f'{self.__class__.__name__}({bbrepr(self.spec)})' _RE_FULLMATCH = getattr(re, "fullmatch", None) -_RE_VALID_FUNCS = set((_RE_FULLMATCH, None, re.search, re.match)) +_RE_VALID_FUNCS = {_RE_FULLMATCH, None, re.search, re.match} _RE_FUNC_ERROR = ValueError("'func' must be one of %s" % (", ".join( sorted(e and e.__name__ or "None" for e in _RE_VALID_FUNCS)))) _RE_TYPES = () -try: re.match(u"", u"") +try: re.match("", "") except Exception: pass # pragma: no cover -else: _RE_TYPES += (type(u""),) +else: _RE_TYPES += (str,) try: re.match(b"", b"") except Exception: pass # pragma: no cover -else: _RE_TYPES += (type(b""),) +else: _RE_TYPES += (bytes,) -class Regex(object): +class Regex: """ checks that target is a string which matches the passed regex pattern @@ -226,7 +226,7 @@ def __init__(self, pattern, flags=0, func=None): if _RE_FULLMATCH: match_func = regex.fullmatch else: - regex = re.compile(r"(?:{})\Z".format(pattern), flags) + regex = re.compile(fr"(?:{pattern})\Z", flags) match_func = regex.match self.flags, self.func = flags, func self.match_func, self.pattern = match_func, pattern @@ -260,7 +260,7 @@ def _bool_child_repr(child): return bbrepr(child) -class _Bool(object): +class _Bool: def __init__(self, *children, **kw): self.children = children if not children: @@ -299,7 +299,7 @@ def _m_repr(self): def __repr__(self): child_reprs = [_bool_child_repr(c) for c in self.children] if self._m_repr() and self.default is _MISSING: - return " {} ".format(self.OP).join(child_reprs) + return f" {self.OP} ".join(child_reprs) if self.default is not _MISSING: child_reprs.append("default=" + repr(self.default)) return self.__class__.__name__ + "(" + ", ".join(child_reprs) + ")" @@ -386,7 +386,7 @@ def __repr__(self): _M_OP_MAP = {'=': '==', '!': '!=', 'g': '>=', 'l': '<='} -class _MSubspec(object): +class _MSubspec: """used by MType.__call__ to wrap a sub-spec for comparison""" __slots__ = ('spec') @@ -412,7 +412,7 @@ def __le__(self, other): return _MExpr(self, 'l', other) def __repr__(self): - return 'M(%s)' % (bbrepr(self.spec),) + return f'M({bbrepr(self.spec)})' def glomit(self, target, scope): match = scope[glom](target, self.spec, scope) @@ -421,7 +421,7 @@ def glomit(self, target, scope): raise MatchError('expected truthy value from {0!r}, got {1!r}', self.spec, match) -class _MExpr(object): +class _MExpr: __slots__ = ('lhs', 'op', 'rhs') def __init__(self, lhs, op, rhs): @@ -462,10 +462,10 @@ def glomit(self, target, scope): def __repr__(self): op = _M_OP_MAP.get(self.op, self.op) - return "{!r} {} {!r}".format(self.lhs, op, self.rhs) + return f"{self.lhs!r} {op} {self.rhs!r}" -class _MType(object): +class _MType: """:attr:`~glom.M` is similar to :attr:`~glom.T`, a stand-in for the current target, but where :attr:`~glom.T` allows for attribute and key access and method calls, :attr:`~glom.M` allows for comparison @@ -568,7 +568,7 @@ def glomit(self, target, spec): -class Optional(object): +class Optional: """Used as a :class:`dict` key in a :class:`~glom.Match()` spec, marks that a value match key which would otherwise be required is optional and should not raise :exc:`~glom.MatchError` even if no @@ -593,7 +593,7 @@ def __init__(self, key, default=_MISSING): raise TypeError("double wrapping of Optional") hash(key) # ensure is a valid key if _precedence(key) != 0: - raise ValueError("Optional() keys must be == match constants, not {!r}".format(key)) + raise ValueError(f"Optional() keys must be == match constants, not {key!r}") self.key, self.default = key, default def glomit(self, target, scope): @@ -602,10 +602,10 @@ def glomit(self, target, scope): return target def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, bbrepr(self.key)) + return f'{self.__class__.__name__}({bbrepr(self.key)})' -class Required(object): +class Required: """Used as a :class:`dict` key in :class:`~glom.Match()` mode, marks that a key which might otherwise not be required should raise :exc:`~glom.MatchError` if the key in the target does not match. @@ -651,7 +651,7 @@ def __init__(self, key): self.key = key def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, bbrepr(self.key)) + return f'{self.__class__.__name__}({bbrepr(self.key)})' def _precedence(match): @@ -761,7 +761,7 @@ def _glom_match(target, spec, scope): return target -class Switch(object): +class Switch: r"""The :class:`Switch` specifier type routes data processing based on matching keys, much like the classic switch statement. @@ -857,13 +857,13 @@ def glomit(self, target, scope): raise MatchError("no matches for target in %s" % self.__class__.__name__) def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, bbrepr(self.cases)) + return f'{self.__class__.__name__}({bbrepr(self.cases)})' RAISE = make_sentinel('RAISE') # flag object for "raise on check failure" -class Check(object): +class Check: """Check objects are used to make assertions about the target data, and either pass through the data or raise exceptions if there is a problem. @@ -972,9 +972,9 @@ def glomit(self, target, scope): if self.default is not RAISE: return arg_val(target, self.default, scope) if len(self.vals) == 1: - errs.append("expected {}, found {}".format(self.vals[0], target)) + errs.append(f"expected {self.vals[0]}, found {target}") else: - errs.append('expected one of {}, found {}'.format(self.vals, target)) + errs.append(f'expected one of {self.vals}, found {target}') if self.validators: for i, validator in enumerate(self.validators): @@ -1042,13 +1042,13 @@ def __init__(self, msgs, check, path): def get_message(self): msg = 'target at path %s failed check,' % self.path if self.check_obj.spec is not T: - msg += ' subtarget at %r' % (self.check_obj.spec,) + msg += f' subtarget at {self.check_obj.spec!r}' if len(self.msgs) == 1: - msg += ' got error: %r' % (self.msgs[0],) + msg += f' got error: {self.msgs[0]!r}' else: - msg += ' got %s errors: %r' % (len(self.msgs), self.msgs) + msg += f' got {len(self.msgs)} errors: {self.msgs!r}' return msg def __repr__(self): cn = self.__class__.__name__ - return '%s(%r, %r, %r)' % (cn, self.msgs, self.check_obj, self.path) + return f'{cn}({self.msgs!r}, {self.check_obj!r}, {self.path!r})' diff --git a/glom/mutation.py b/glom/mutation.py index 554cbbdf..c3fc322e 100644 --- a/glom/mutation.py +++ b/glom/mutation.py @@ -58,7 +58,7 @@ def _apply_for_each(func, path, val): func(val) -class Assign(object): +class Assign: """*New in glom 18.3.0* The ``Assign`` specifier type enables glom to modify the target, @@ -146,7 +146,7 @@ def __init__(self, path, val, missing=None): if missing is not None: if not callable(missing): - raise TypeError('expected missing to be callable, not %r' % (missing,)) + raise TypeError(f'expected missing to be callable, not {missing!r}') self.missing = missing def glomit(self, target, scope): @@ -182,8 +182,8 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ if self.missing is None: - return '%s(%r, %r)' % (cn, self._orig_path, self.val) - return '%s(%r, %r, missing=%s)' % (cn, self._orig_path, self.val, bbrepr(self.missing)) + return f'{cn}({self._orig_path!r}, {self.val!r})' + return f'{cn}({self._orig_path!r}, {self.val!r}, missing={bbrepr(self.missing)})' def assign(obj, path, val, missing=None): @@ -209,7 +209,7 @@ def assign(obj, path, val, missing=None): if not issubclass(v, tuple([t for t in _ALL_BUILTIN_TYPES if t not in (v, type, object)]))] _UNASSIGNABLE_BASE_TYPES = tuple(set(_BUILTIN_BASE_TYPES) - - set([dict, list, BaseException, object, type])) + - {dict, list, BaseException, object, type}) def _set_sequence_item(target, idx, val): @@ -232,7 +232,7 @@ def _assign_autodiscover(type_obj): register_op('assign', auto_func=_assign_autodiscover, exact=False) -class Delete(object): +class Delete: """ In addition to glom's core "deep-get" and ``Assign``'s "deep-set", the ``Delete`` specifier type performs a "deep-del", which can @@ -320,7 +320,7 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ - return '%s(%r)' % (cn, self._orig_path) + return f'{cn}({self._orig_path!r})' def delete(obj, path, ignore_missing=False): diff --git a/glom/reduction.py b/glom/reduction.py index 7be12439..b400ce0d 100644 --- a/glom/reduction.py +++ b/glom/reduction.py @@ -1,4 +1,3 @@ - import operator import itertools from pprint import pprint @@ -23,7 +22,7 @@ class FoldError(GlomError): pass -class Fold(object): +class Fold: """The `Fold` specifier type is glom's building block for reducing iterables in data, implementing the classic `fold `_ @@ -124,7 +123,7 @@ class Sum(Fold): """ def __init__(self, subspec=T, init=int): - super(Sum, self).__init__(subspec=subspec, init=init, op=operator.iadd) + super().__init__(subspec=subspec, init=init, op=operator.iadd) def __repr__(self): cn = self.__class__.__name__ @@ -143,7 +142,7 @@ class Count(Fold): __slots__ = () def __init__(self): - super(Count, self).__init__( + super().__init__( subspec=T, init=int, op=lambda cur, val: cur + 1) def __repr__(self): @@ -169,12 +168,12 @@ def __init__(self, subspec=T, init=list): init = list else: self.lazy = False - super(Flatten, self).__init__(subspec=subspec, init=init, op=operator.iadd) + super().__init__(subspec=subspec, init=init, op=operator.iadd) def _fold(self, iterator): if self.lazy: return itertools.chain.from_iterable(iterator) - return super(Flatten, self)._fold(iterator) + return super()._fold(iterator) def __repr__(self): cn = self.__class__.__name__ @@ -298,7 +297,7 @@ def __init__(self, subspec=T, init=dict, op=None): if not callable(op): raise ValueError('expected callable "op" arg or an "init" with an .update()' ' method not %r and %r' % (op, init)) - super(Merge, self).__init__(subspec=subspec, init=init, op=op) + super().__init__(subspec=subspec, init=init, op=op) def _fold(self, iterator): # the difference here is that ret is mutated in-place, the diff --git a/glom/streaming.py b/glom/streaming.py index dffc78a8..4da5fb50 100644 --- a/glom/streaming.py +++ b/glom/streaming.py @@ -24,7 +24,7 @@ from .core import glom, T, STOP, SKIP, _MISSING, Path, TargetRegistry, Call, Spec, Pipe, S, bbrepr, format_invocation from .matching import Check -class Iter(object): +class Iter: """``Iter()`` is glom's counterpart to Python's built-in :func:`iter()` function. Given an iterable target, ``Iter()`` yields the result of applying the passed spec to each element of the target, similar @@ -275,7 +275,7 @@ def slice(self, *args): try: islice([], *args) except TypeError: - raise TypeError('invalid slice arguments: %r' % (args,)) + raise TypeError(f'invalid slice arguments: {args!r}') return self._add_op('slice', args, lambda it, scope: islice(it, *args)) def limit(self, count): @@ -352,7 +352,7 @@ def first(self, key=T, default=None): return (self, First(key=key, default=default)) -class First(object): +class First: """Get the first element of an iterable which matches *key*, if there is one, otherwise return *default* (``None`` if unset). @@ -381,5 +381,5 @@ def glomit(self, target, scope): def __repr__(self): cn = self.__class__.__name__ if self._default is None: - return '%s(%s)' % (cn, bbrepr(self._spec)) - return '%s(%s, default=%s)' % (cn, bbrepr(self._spec), bbrepr(self._default)) + return f'{cn}({bbrepr(self._spec)})' + return f'{cn}({bbrepr(self._spec)}, default={bbrepr(self._default)})' diff --git a/glom/test/perf_report.py b/glom/test/perf_report.py index 6315fb5e..6c555e5a 100644 --- a/glom/test/perf_report.py +++ b/glom/test/perf_report.py @@ -53,7 +53,7 @@ def run(spec, data): start = time.time() glom(data, spec) end = time.time() - print("{} us per object".format((end - start) / len(data) * 1e6)) + print(f"{(end - start) / len(data) * 1e6} us per object") def ratio(spec, func, data): diff --git a/glom/test/test_basic.py b/glom/test/test_basic.py index bce5e38a..79556b74 100644 --- a/glom/test/test_basic.py +++ b/glom/test/test_basic.py @@ -1,6 +1,5 @@ - import sys -from xml.etree import cElementTree as ElementTree +from xml.etree import ElementTree as ElementTree import pytest @@ -15,7 +14,7 @@ def test_initial_integration(): - class Example(object): + class Example: pass example = Example() @@ -180,7 +179,7 @@ def test_val(): def test_abstract_iterable(): assert isinstance([], glom_core._AbstractIterable) - class MyIterable(object): + class MyIterable: def __iter__(self): return iter([1, 2, 3]) mi = MyIterable() @@ -190,14 +189,14 @@ def __iter__(self): def test_call_and_target(): - class F(object): + class F: def __init__(s, a, b, c): s.a, s.b, s.c = a, b, c call_f = Call(F, kwargs=dict(a=T, b=T, c=T)) assert repr(call_f) val = glom(1, call_f) assert (val.a, val.b, val.c) == (1, 1, 1) - class F(object): + class F: def __init__(s, a): s.a = a val = glom({'one': F('two')}, Call(F, args=(T['one'].a,))) assert val.a == 'two' @@ -358,7 +357,7 @@ def test_python_native(): spec = T['system']['planets'][-1].values() output = glom(target, spec) - assert set(output) == set(['jupiter', 69]) # for ordering reasons + assert set(output) == {'jupiter', 69} # for ordering reasons with pytest.raises(glom_core.GlomError): spec = T['system']['comets'][-1].values() @@ -444,7 +443,7 @@ def test_api_repr(): if v.__repr__ is object.__repr__: spec_types_wo_reprs.append(k) # pragma: no cover - assert set(spec_types_wo_reprs) == set([]) + assert set(spec_types_wo_reprs) == set() def test_bbformat(): diff --git a/glom/test/test_check.py b/glom/test/test_check.py index f498442f..7b08a02f 100644 --- a/glom/test/test_check.py +++ b/glom/test/test_check.py @@ -1,4 +1,3 @@ - from pytest import raises from glom import glom, Check, CheckError, Coalesce, SKIP, STOP, T @@ -32,7 +31,7 @@ def test_check_basic(): assert repr(Check(instance_of=dict)) == 'Check(instance_of=dict)' assert repr(Check(T(len), validate=sum)) == 'Check(T(len), validate=sum)' - target = [1, u'a'] + target = [1, 'a'] assert glom(target, [Check(type=unicode, default=SKIP)]) == ['a'] assert glom(target, [Check(type=(unicode, int))]) == [1, 'a'] assert glom(target, [Check(instance_of=unicode, default=SKIP)]) == ['a'] diff --git a/glom/test/test_cli.py b/glom/test/test_cli.py index bfd78c7f..aef5bff4 100644 --- a/glom/test/test_cli.py +++ b/glom/test/test_cli.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals import os import subprocess diff --git a/glom/test/test_error.py b/glom/test/test_error.py index bdb91112..42263542 100644 --- a/glom/test/test_error.py +++ b/glom/test/test_error.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import re import sys @@ -60,12 +59,12 @@ def _norm_stack(formatted_stack, exc): normalized = [] for line in formatted_stack.splitlines(): - if line.strip().startswith(u'File'): - file_name = line.split(u'"')[1] - short_file_name = os.path.split(file_name.strip(u'"'))[1] + if line.strip().startswith('File'): + file_name = line.split('"')[1] + short_file_name = os.path.split(file_name.strip('"'))[1] line = line.replace(file_name, short_file_name) - line = line.partition(u'line')[0] + u'line ___,' + line.partition(u'line')[2].partition(u',')[2] - line = line.partition(u'0x')[0] # scrub memory addresses + line = line.partition('line')[0] + 'line ___,' + line.partition('line')[2].partition(',')[2] + line = line.partition('0x')[0] # scrub memory addresses line = line.rstrip() # trailing whitespace shouldn't matter @@ -81,8 +80,8 @@ def _norm_stack(formatted_stack, exc): normalized.append(line) - stack = u"\n".join(normalized) + u'\n' - stack = stack.replace(u',)', u')') # py37 likes to do Exception('msg',) + stack = "\n".join(normalized) + '\n' + stack = stack.replace(',)', ')') # py37 likes to do Exception('msg',) return stack @@ -97,11 +96,11 @@ def _debug_some_str(value, *a, **kw): return str(value) except BaseException as be: # pragma: no cover try: - print(' !! failed to stringify %s object, got %s' % (type(value).__name__, be)) + print(f' !! failed to stringify {type(value).__name__} object, got {be}') traceback.print_exc() except: print(' !! unable to print trace') - return '' % (type(value).__name__, be) + return f'' traceback._some_str = _debug_some_str @@ -156,7 +155,7 @@ def test_regular_error_stack(): def test_glom_error_stack(): # NoneType has not attribute value - expected = u"""\ + expected = """\ Traceback (most recent call last): File "test_error.py", line ___, in _make_stack glom(target, spec) @@ -174,7 +173,7 @@ def test_glom_error_stack(): """ #import glom.core #glom.core.GLOM_DEBUG = True - actual = _make_stack({'results': [{u'valué': u'value'}]}) + actual = _make_stack({'results': [{'valué': 'value'}]}) print(actual) assert actual == expected @@ -230,9 +229,9 @@ def test_long_target_repr(): actual = _make_stack(target=[None] * 1000, spec='1001') assert '(len=1000)' in actual - class ObjectWithLongRepr(object): + class ObjectWithLongRepr: def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, 'w' + ('ooooo' * 250)) + return '<{} {}>'.format(self.__class__.__name__, 'w' + ('ooooo' * 250)) actual = _make_stack(target=ObjectWithLongRepr(), spec='badattr') assert '...' in actual @@ -411,7 +410,7 @@ def __init__(self, first): if not first: 1/0 self.first = False - super(BadExc, self).__init__(self.first) + super().__init__(self.first) bad_exc = BadExc(True) @@ -503,10 +502,10 @@ def test_glom_dev_debug(): def test_unicode_stack(): - val = {u'resumé': u'beyoncé'} - stack = _make_stack(target=val, spec=u'a.é.i.o') + val = {'resumé': 'beyoncé'} + stack = _make_stack(target=val, spec='a.é.i.o') assert 'beyonc' in stack - assert u'é' in stack + assert 'é' in stack def test_3_11_byte_code_caret(): diff --git a/glom/test/test_grouping.py b/glom/test/test_grouping.py index f80c408a..eb7e6915 100644 --- a/glom/test/test_grouping.py +++ b/glom/test/test_grouping.py @@ -1,5 +1,3 @@ -from __future__ import division - from pytest import raises from glom import glom, T, SKIP, STOP, Auto, BadSpec, Val diff --git a/glom/test/test_match.py b/glom/test/test_match.py index e31dfd8c..f22af56c 100644 --- a/glom/test/test_match.py +++ b/glom/test/test_match.py @@ -1,4 +1,3 @@ - import re import json @@ -396,7 +395,7 @@ def test_nested_struct(): Match(Or( And(dict, {Ref('json'): Ref('json')}), And(list, [Ref('json')]), - And(type(u''), Auto(str)), + And(str, Auto(str)), object))) rule_spec = Match({ @@ -448,7 +447,7 @@ def test_check_ported_tests(): assert glom('hello', spec, glom_debug=True) == 'h' assert glom('', spec) == '' # would fail with IndexError if STOP didn't work - target = [1, u'a'] + target = [1, 'a'] assert glom(target, [Match(unicode, default=SKIP)]) == ['a'] assert glom(target, Match([Or(unicode, int)])) == [1, 'a'] diff --git a/glom/test/test_mutation.py b/glom/test/test_mutation.py index ae0e3ed8..ec86b391 100644 --- a/glom/test/test_mutation.py +++ b/glom/test/test_mutation.py @@ -7,7 +7,7 @@ def test_assign(): - class Foo(object): + class Foo: pass assert glom({}, Assign(T['a'], 1)) == {'a': 1} @@ -63,7 +63,7 @@ def test_unregistered_assign(): def test_bad_assign_target(): - class BadTarget(object): + class BadTarget: def __setattr__(self, name, val): raise Exception("and you trusted me?") @@ -135,7 +135,7 @@ def debugdict(): def test_assign_missing_object(): val = object() - class Container(object): + class Container: pass target = Container() @@ -177,7 +177,7 @@ def test_assign_missing_unassignable(): """ - class Tarjay(object): + class Tarjay: init_count = 0 def __init__(self): self.__class__.init_count += 1 @@ -209,7 +209,7 @@ def test_s_assign(): def test_delete(): - class Foo(object): + class Foo: def __init__(self, d=None): for k, v in d.items(): setattr(self, k, v) @@ -271,7 +271,7 @@ def test_unregistered_delete(): def test_bad_delete_target(): - class BadTarget(object): + class BadTarget: def __delattr__(self, name): raise Exception("and you trusted me?") diff --git a/glom/test/test_path_and_t.py b/glom/test/test_path_and_t.py index ef09a51d..793489e8 100644 --- a/glom/test/test_path_and_t.py +++ b/glom/test/test_path_and_t.py @@ -1,4 +1,3 @@ - from pytest import raises from glom import glom, Path, S, T, A, PathAccessError, GlomError, BadSpec, Or, Assign, Delete @@ -88,7 +87,7 @@ def test_path_access_error_message(): def test_t_picklability(): import pickle - class TargetType(object): + class TargetType: def __init__(self): self.attribute = lambda: None self.attribute.method = lambda: {'key': lambda x: x * 2} diff --git a/glom/test/test_reduction.py b/glom/test/test_reduction.py index e1fcadb5..67fefe7f 100644 --- a/glom/test/test_reduction.py +++ b/glom/test/test_reduction.py @@ -1,4 +1,3 @@ - import operator import pytest diff --git a/glom/test/test_scope_vars.py b/glom/test/test_scope_vars.py index 70a7ee20..68af1ab5 100644 --- a/glom/test/test_scope_vars.py +++ b/glom/test/test_scope_vars.py @@ -1,4 +1,3 @@ - import pytest from glom import glom, Path, S, A, T, Vars, Val, GlomError, M, SKIP, Let @@ -22,7 +21,7 @@ def test_s_scope_assign(): with pytest.raises(GlomError): glom(1, (S(v=1), A.v.a)) - class FailAssign(object): + class FailAssign: def __setattr__(self, name, val): raise Exception('nope') @@ -97,7 +96,7 @@ def test_let(): # backwards compat 2020-07 with pytest.raises(GlomError): glom(1, (Let(v=lambda t: 1), A.v.a)) - class FailAssign(object): + class FailAssign: def __setattr__(self, name, val): raise Exception('nope') diff --git a/glom/test/test_snippets.py b/glom/test/test_snippets.py index d7219729..7781bbaf 100644 --- a/glom/test/test_snippets.py +++ b/glom/test/test_snippets.py @@ -23,7 +23,7 @@ def _get_codeblock(lines, offset): def _find_snippets(): path = os.path.dirname(os.path.abspath(__file__)) + '/../../docs/snippets.rst' - with open(path, 'r') as snippet_file: + with open(path) as snippet_file: lines = list(snippet_file) snippets = [] for line_no in range(len(lines)): diff --git a/glom/test/test_spec.py b/glom/test/test_spec.py index b79c6fd0..d9cc9a12 100644 --- a/glom/test/test_spec.py +++ b/glom/test/test_spec.py @@ -1,4 +1,3 @@ - import pytest from glom import glom, Spec, T, S diff --git a/glom/test/test_streaming.py b/glom/test/test_streaming.py index 9972cce8..a673dacf 100644 --- a/glom/test/test_streaming.py +++ b/glom/test/test_streaming.py @@ -1,4 +1,3 @@ - import pytest from itertools import count, dropwhile, chain diff --git a/glom/test/test_target_types.py b/glom/test/test_target_types.py index b8a19c51..d8da9490 100644 --- a/glom/test/test_target_types.py +++ b/glom/test/test_target_types.py @@ -1,6 +1,3 @@ - -from __future__ import print_function - import pytest import glom @@ -8,10 +5,10 @@ from glom.core import TargetRegistry -class A(object): +class A: pass -class B(object): +class B: pass class C(A): @@ -106,7 +103,7 @@ class BetterList(list): def test_duck_register(): - class LilRanger(object): + class LilRanger: def __init__(self): self.lil_list = list(range(5)) @@ -205,7 +202,7 @@ def test_faulty_op_registration(): with pytest.raises(TypeError, match="callable, not:"): treg.register_op('fake_op', object()) - class NewType(object): + class NewType: pass def _autodiscover_raise(type_obj): @@ -254,7 +251,7 @@ def _autodiscover_sneaky(type_obj): def test_reregister_type(): treg = TargetRegistry() - class NewType(object): + class NewType: pass treg.register(NewType, op=lambda obj: obj) diff --git a/glom/test/test_tutorial.py b/glom/test/test_tutorial.py index 98490af9..949d9482 100644 --- a/glom/test/test_tutorial.py +++ b/glom/test/test_tutorial.py @@ -1,4 +1,3 @@ - from glom import glom, tutorial from glom.tutorial import Contact, Email diff --git a/glom/tutorial.py b/glom/tutorial.py index efc19f73..514035ca 100644 --- a/glom/tutorial.py +++ b/glom/tutorial.py @@ -449,7 +449,7 @@ def _default_email(contact): @attr.s -class ContactManager(object): +class ContactManager: """This type implements an oversimplified storage manager, wrapping an OrderedDict instead of a database. Those familiar with Django and SQLAlchemy will recognize the pattern being sketched here. @@ -466,7 +466,7 @@ def get(self, contact_id): @attr.s -class Contact(object): +class Contact: id = attr.ib(Factory(_contact_autoincrement), init=False) name = attr.ib('') pref_name = attr.ib('') @@ -487,7 +487,7 @@ def save(self): @attr.s -class Email(object): +class Email: id = attr.ib(Factory(_email_autoincrement), init=False) email = attr.ib('') email_type = attr.ib('personal') diff --git a/requirements.txt b/requirements.txt index e8dfbde3..bdae7314 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,28 +2,73 @@ # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # -# pip-compile --annotation-style=line --strip-extras requirements.in +# pip-compile requirements.in # -attrs==23.1.0 # via -r requirements.in -boltons==23.1.1 # via -r requirements.in, face -cachetools==5.3.2 # via tox -chardet==5.2.0 # via tox -colorama==0.4.6 # via tox -coverage==7.2.7 # via -r requirements.in -distlib==0.3.8 # via virtualenv -exceptiongroup==1.2.0 # via pytest -face==22.0.0 # via -r requirements.in -filelock==3.12.2 # via tox, virtualenv -importlib-metadata==6.7.0 # via attrs, pluggy, pytest, tox, virtualenv -iniconfig==2.0.0 # via pytest -packaging==23.2 # via pyproject-api, pytest, tox -platformdirs==4.0.0 # via tox, virtualenv -pluggy==1.2.0 # via pytest, tox -pyproject-api==1.5.3 # via tox -pytest==7.4.3 # via -r requirements.in -pyyaml==6.0.1 # via -r requirements.in -tomli==2.0.1 # via pyproject-api, pytest, tox -tox==4.8.0 # via -r requirements.in -typing-extensions==4.7.1 # via importlib-metadata, platformdirs, tox -virtualenv==20.25.0 # via tox -zipp==3.15.0 # via importlib-metadata +attrs==24.2.0 + # via -r requirements.in +boltons==24.1.0 + # via + # -r requirements.in + # face +cachetools==5.5.0 + # via tox +chardet==5.2.0 + # via tox +colorama==0.4.6 + # via tox +coverage==7.2.7 + # via -r requirements.in +distlib==0.3.9 + # via virtualenv +exceptiongroup==1.2.2 + # via pytest +face==24.0.0 + # via -r requirements.in +filelock==3.12.2 + # via + # tox + # virtualenv +importlib-metadata==6.7.0 + # via + # attrs + # pluggy + # pytest + # tox + # virtualenv +iniconfig==2.0.0 + # via pytest +packaging==24.0 + # via + # pyproject-api + # pytest + # tox +platformdirs==4.0.0 + # via + # tox + # virtualenv +pluggy==1.2.0 + # via + # pytest + # tox +pyproject-api==1.5.3 + # via tox +pytest==7.4.4 + # via -r requirements.in +pyyaml==6.0.1 + # via -r requirements.in +tomli==2.0.1 + # via + # pyproject-api + # pytest + # tox +tox==4.8.0 + # via -r requirements.in +typing-extensions==4.7.1 + # via + # importlib-metadata + # platformdirs + # tox +virtualenv==20.26.6 + # via tox +zipp==3.15.0 + # via importlib-metadata diff --git a/setup.py b/setup.py index 1f33a28e..07b194fd 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ def import_path(module_name, path): 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'License :: OSI Approved :: BSD License', @@ -83,4 +84,6 @@ def import_path(module_name, path): * git commit * git push +NB: if dropping support for a python version, bump the pyupgrade argument in tox and run syntax-upgrade tox env. + """ diff --git a/tox.ini b/tox.ini index 6520e843..6b225566 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,pypy3,coverage-report,packaging +envlist = py37,py38,py39,py310,py311,py312,pypy3,coverage-report,packaging [testenv] changedir = .tox @@ -18,6 +18,15 @@ commands = coverage combine --rcfile {toxinidir}/.tox-coveragerc [testenv:packaging] changedir = {toxinidir} deps = - check-manifest==0.40 + check-manifest==0.50 commands = check-manifest + +[testenv:syntax-upgrade] +changedir = {toxinidir} +deps = + flynt + pyupgrade +commands = + flynt ./glom + python -c "import glob; import subprocess; [subprocess.run(['pyupgrade', '--py37-plus', f]) for f in glob.glob('./glom/**/*.py', recursive=True)]" \ No newline at end of file