diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f991135..d849bf8 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -18,17 +18,17 @@ 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: 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 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: update pip @@ -40,7 +40,7 @@ jobs: id: pip-cache run: echo "::set-output name=dir::$(pip cache dir)" - name: cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}|${{ hashFiles('requirements/*.txt') }} diff --git a/README.md b/README.md index 1945f45..3d2df55 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ glom is a new and powerful way to handle real-world data, featuring: * Built-in data exploration and debugging features All of that and more, available as a [fully-documented][rtd], -pure-Python package, tested on Python 3.7+, as well as +pure-Python package, tested on Python 3.8+, as well as PyPy3. Installation is as easy as: ``` diff --git a/docs/conf.py b/docs/conf.py index 50c98b9..76a9040 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # @@ -31,14 +30,14 @@ # -- Project information ----------------------------------------------------- -project = u'glom' -copyright = u'2023, Mahmoud Hashemi' -author = u'Mahmoud Hashemi' +project = 'glom' +copyright = '2023, Mahmoud Hashemi' +author = 'Mahmoud Hashemi' # The short X.Y version -version = u'23.5' +version = '23.5' # The full version, including alpha/beta/rc tags -release = u'23.5.0' +release = '23.5.0' todo_add_to_theme_to_keep_menus_expanded = """ @@ -95,7 +94,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'default' @@ -167,8 +166,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'glom.tex', u'glom Documentation', - u'Mahmoud Hashemi', 'manual'), + (master_doc, 'glom.tex', 'glom Documentation', + 'Mahmoud Hashemi', 'manual'), ] @@ -177,7 +176,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'glom', u'glom Documentation', + (master_doc, 'glom', 'glom Documentation', [author], 1) ] @@ -188,7 +187,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'glom', u'glom Documentation', + (master_doc, 'glom', 'glom Documentation', author, 'glom', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/index.rst b/docs/index.rst index 54683b9..c8a6d16 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,7 +22,7 @@ or `try glom in your browser now`__! Installation ------------ -glom is pure Python, and tested on Python 3.7+, as well as +glom is pure Python, and tested on Python 3.8+, as well as PyPy3. Installation is easy:: pip install glom diff --git a/glom/__init__.py b/glom/__init__.py index 77bd1ed..1fb4035 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 a9c2d64..9935c57 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 3f8f744..7e87e85 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): 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: @@ -170,10 +169,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() @@ -195,9 +194,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() @@ -218,7 +217,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 1f8a91e..e9b7446 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() @@ -233,7 +232,7 @@ def _format_trace_value(value, maxlen): s = bbrepr(value).replace("\\'", "'") if len(s) > maxlen: try: - suffix = '... (len=%s)' % len(value) + suffix = f'... (len={len(value)})' except Exception: suffix = '...' s = s[:maxlen - len(suffix)] + suffix @@ -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,22 +422,22 @@ 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) skipped_vals = [v.__class__.__name__ if isinstance(v, self.coal_obj.skip_exc) - else '' % v.__class__.__name__ + else f'' for v in self.skipped] 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__ @@ -489,11 +488,11 @@ def get_message(self): return ("glom() called without registering any types for operation '%s'. see" " glom.register() or Glommer's constructor for details." % (self.op,)) reg_types = sorted([t.__name__ for t, h in self.type_map.items() if h]) - reg_types_str = '()' if not reg_types else ('(%s)' % ', '.join(reg_types)) + reg_types_str = '()' if not reg_types else (f"({', '.join(reg_types)})") 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 @@ -558,24 +557,24 @@ def format_invocation(name='', args=(), kwargs=None, **kw): """ _repr = kw.pop('repr', bbrepr) if kw: - raise TypeError('unexpected keyword args: %r' % ', '.join(kw.keys())) + raise TypeError(f"unexpected keyword args: {', '.join(kw.keys())!r}") kwargs = kwargs or {} a_text = ', '.join([_repr(a) for a in args]) if isinstance(kwargs, dict): 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 @@ -997,13 +996,13 @@ def __init__(self, *a, **kw): if breakpoint is True: breakpoint = pdb.set_trace if breakpoint and not callable(breakpoint): - raise TypeError('breakpoint expected bool or callable, not: %r' % breakpoint) + raise TypeError(f'breakpoint expected bool or callable, not: {breakpoint!r}') self.breakpoint = breakpoint post_mortem = kw.pop('post_mortem', False) if post_mortem is True: post_mortem = pdb.post_mortem if post_mortem and not callable(post_mortem): - raise TypeError('post_mortem expected bool or callable, not: %r' % post_mortem) + raise TypeError(f'post_mortem expected bool or callable, not: {post_mortem!r}') self.post_mortem = post_mortem def __repr__(self): @@ -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 @@ -2269,7 +2268,7 @@ def glom(target, spec, **kwargs): scope.update(kwargs.pop('scope', {})) err = None if kwargs: - raise TypeError('unexpected keyword args: %r' % sorted(kwargs.keys())) + raise TypeError(f'unexpected keyword args: {sorted(kwargs.keys())!r}') try: try: ret = _glom(target, spec, scope) @@ -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 650dd60..12c2146 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 @@ -172,10 +171,10 @@ def agg(self, target, tree): return STOP def __repr__(self): - return '%s()' % self.__class__.__name__ + return f'{self.__class__.__name__}()' -class Avg(object): +class Avg: """ takes the numerical average of all values; raises exception on non-numeric value @@ -196,10 +195,10 @@ def agg(self, target, tree): return avg_acc[0] / avg_acc[1] def __repr__(self): - return '%s()' % self.__class__.__name__ + return f'{self.__class__.__name__}()' -class Max(object): +class Max: """ takes the maximum of all values; raises exception on values that are not comparable @@ -215,10 +214,10 @@ def agg(self, target, tree): return tree[self] def __repr__(self): - return '%s()' % self.__class__.__name__ + return f'{self.__class__.__name__}()' -class Min(object): +class Min: """ takes the minimum of all values; raises exception on values that are not comparable @@ -234,10 +233,10 @@ def agg(self, target, tree): return tree[self] def __repr__(self): - return '%s()' % self.__class__.__name__ + return f'{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 0a42719..4c44f18 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,15 +260,14 @@ def _bool_child_repr(child): return bbrepr(child) -class _Bool(object): +class _Bool: def __init__(self, *children, **kw): self.children = children if not children: - raise ValueError("need at least one operand for {}".format( - self.__class__.__name__)) + raise ValueError(f"need at least one operand for {self.__class__.__name__}") self.default = kw.pop('default', _MISSING) if kw: - raise TypeError('got unexpected kwargs: %r' % list(kw.keys())) + raise TypeError(f'got unexpected kwargs: {list(kw.keys())!r}') def __and__(self, other): return And(self, other) @@ -299,7 +298,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 +385,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 +411,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 +420,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 +461,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 @@ -523,7 +522,7 @@ def __call__(self, spec): if not isinstance(spec, type(T)): # TODO: open this up for other specs so we can do other # checks, like function calls - raise TypeError("M() only accepts T-style specs, not %s" % type(spec).__name__) + raise TypeError(f"M() only accepts T-style specs, not {type(spec).__name__}") return _MSubspec(spec) def __eq__(self, other): @@ -568,7 +567,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 +592,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 +601,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 +650,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 +760,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. @@ -854,16 +853,16 @@ def glomit(self, target, scope): return scope[glom](target, valspec, chain_child(scope)) if self.default is not _MISSING: return arg_val(target, self.default, scope) - raise MatchError("no matches for target in %s" % self.__class__.__name__) + raise MatchError(f"no matches for target in {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. @@ -926,7 +925,7 @@ def truthy(val): equal_to = kwargs.pop('equal_to', _MISSING) one_of = kwargs.pop('one_of', _MISSING) if kwargs: - raise TypeError('unexpected keyword arguments: %r' % kwargs.keys()) + raise TypeError(f'unexpected keyword arguments: {kwargs.keys()!r}') self.validators = _get_arg_val('validate', 'callable', callable, validate) self.instance_of = _get_arg_val('instance_of', 'a type', @@ -972,9 +971,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): @@ -984,12 +983,12 @@ def glomit(self, target, scope): raise self._ValidationError except Exception as e: msg = ('expected %r check to validate target' - % getattr(validator, '__name__', None) or ('#%s' % i)) + % getattr(validator, '__name__', None) or (f'#{i}')) if type(e) is self._ValidationError: if self.default is not RAISE: return self.default else: - msg += ' (got exception: %r)' % e + msg += f' (got exception: {e!r})' errs.append(msg) if self.instance_of and not isinstance(target, self.instance_of): @@ -1040,15 +1039,15 @@ def __init__(self, msgs, check, path): self.path = path def get_message(self): - msg = 'target at path %s failed check,' % self.path + msg = f'target at path {self.path} failed check,' 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 554cbbd..c3fc322 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 7be1243..ca0d3bd 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,11 +142,11 @@ 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): - return '%s()' % self.__class__.__name__ + return f'{self.__class__.__name__}()' class Flatten(Fold): @@ -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__ @@ -251,12 +250,12 @@ def flatten(target, **kwargs): init = kwargs.pop('init', list) levels = kwargs.pop('levels', 1) if kwargs: - raise TypeError('unexpected keyword args: %r' % sorted(kwargs.keys())) + raise TypeError(f'unexpected keyword args: {sorted(kwargs.keys())!r}') if levels == 0: return target if levels < 0: - raise ValueError('expected levels >= 0, not %r' % levels) + raise ValueError(f'expected levels >= 0, not {levels!r}') spec = (subspec,) spec += (Flatten(init="lazy"),) * (levels - 1) spec += (Flatten(init=init),) @@ -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 @@ -344,6 +343,6 @@ def merge(target, **kwargs): init = kwargs.pop('init', dict) op = kwargs.pop('op', None) if kwargs: - raise TypeError('unexpected keyword args: %r' % sorted(kwargs.keys())) + raise TypeError(f'unexpected keyword args: {sorted(kwargs.keys())!r}') spec = Merge(subspec, init, op) return glom(target, spec) diff --git a/glom/streaming.py b/glom/streaming.py index dffc78a..e446641 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 @@ -61,7 +61,7 @@ def __init__(self, subspec=T, **kwargs): self.sentinel = kwargs.pop('sentinel', STOP) if kwargs: - raise TypeError('unexpected keyword arguments: %r' % sorted(kwargs)) + raise TypeError(f'unexpected keyword arguments: {sorted(kwargs)!r}') return def __repr__(self): @@ -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 6315fb5..6c555e5 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 bce5e38..79556b7 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 f498442..7b08a02 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 54607a3..9b44634 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 bdb9111..f232193 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 f"<{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 f80c408..eb7e691 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 e31dfd8..f22af56 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 ae0e3ed..3291651 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?") @@ -101,7 +101,7 @@ def test_sequence_assign(): def test_invalid_assign_op_target(): - target = {'afunc': lambda x: 'hi %s' % x} + target = {'afunc': lambda x: f'hi {x}'} spec = T['afunc'](x=1) with pytest.raises(ValueError): @@ -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?") @@ -304,7 +304,7 @@ def test_sequence_delete(): def test_invalid_delete_op_target(): - target = {'afunc': lambda x: 'hi %s' % x} + target = {'afunc': lambda x: f'hi {x}'} spec = T['afunc'](x=1) with pytest.raises(ValueError): diff --git a/glom/test/test_path_and_t.py b/glom/test/test_path_and_t.py index ef09a51..793489e 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 e1fcadb..67fefe7 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 70a7ee2..68af1ab 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 d721972..7781bba 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 b79c6fd..d9cc9a1 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 9972cce..a673dac 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 b8a19c5..d8da949 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 98490af..949d948 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 efc19f7..514035c 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.in b/requirements.in index 781acd8..2e392c7 100644 --- a/requirements.in +++ b/requirements.in @@ -1,6 +1,6 @@ attrs>=19.2.0 boltons>=20.2.0 -coverage<=7.2.7 # can unpin when dropping py37 +coverage face>=20.1.1 pytest>=6.2.5 tox>=3.7.0 diff --git a/requirements.txt b/requirements.txt index e8dfbde..fa74190 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.7 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --annotation-style=line --strip-extras requirements.in @@ -11,10 +11,8 @@ 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 @@ -22,8 +20,5 @@ 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 diff --git a/setup.py b/setup.py index 1f33a28..71c444f 100644 --- a/setup.py +++ b/setup.py @@ -48,16 +48,17 @@ def import_path(module_name, path): zip_safe=False, platforms='any', license_files=['LICENSE'], + python_requires='>=3.8', classifiers=[ 'Topic :: Utilities', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', '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', diff --git a/tox.ini b/tox.ini index 6520e84..d32b814 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,py311,pypy3,coverage-report,packaging +envlist = py38,py39,py310,py311,py312,pypy3,coverage-report,packaging [testenv] changedir = .tox