diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 76c86d5d..471d4bd1 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -9,6 +9,12 @@ on: pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + + jobs: check-formatting: runs-on: ubuntu-latest diff --git a/poetry.lock b/poetry.lock index 68d3a009..1f772823 100644 --- a/poetry.lock +++ b/poetry.lock @@ -431,16 +431,6 @@ files = [ {file = "flaky-3.7.0.tar.gz", hash = "sha256:3ad100780721a1911f57a165809b7ea265a7863305acb66708220820caf8aa0d"}, ] -[[package]] -name = "future" -version = "0.18.3" -description = "Clean single-source support for Python 3 and 2" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, -] - [[package]] name = "identify" version = "2.5.35" @@ -1495,4 +1485,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "2a80fcaa4615afd1da17d46d520b28cc6d0b54a6245ab4e34e4fa20904bc2d82" +content-hash = "bbcd65bc4c3c6670b39d3442bdbdb98569b938ece98d62ecd3287a3e75fb5ee7" diff --git a/pyproject.toml b/pyproject.toml index 79cbbafb..b01b312a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,8 +32,6 @@ termcolor = ">=1.1.0,<2" pyparsing = ">=2.0,<3" clique = "==1.6.1" websocket-client = ">=0.40.0,<1" -future = ">=0.16.0,<1" -six = ">=1.13.0,<2" platformdirs = ">=4.0.0,<5" wheel = "^0.41.2" diff --git a/source/ftrack_api/_weakref.py b/source/ftrack_api/_weakref.py deleted file mode 100644 index 69cc6f4b..00000000 --- a/source/ftrack_api/_weakref.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Yet another backport of WeakMethod for Python 2.7. -Changes include removing exception chaining and adding args to super() calls. - -Copyright (c) 2001-2019 Python Software Foundation.All rights reserved. - -Full license available in LICENSE.python. -""" -from weakref import ref - - -class WeakMethod(ref): - """ - A custom `weakref.ref` subclass which simulates a weak reference to - a bound method, working around the lifetime problem of bound methods. - """ - - __slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__" - - def __new__(cls, meth, callback=None): - try: - obj = meth.__self__ - func = meth.__func__ - except AttributeError: - raise TypeError( - "argument should be a bound method, not {}".format(type(meth)) - ) - - def _cb(arg): - # The self-weakref trick is needed to avoid creating a reference - # cycle. - self = self_wr() - if self._alive: - self._alive = False - if callback is not None: - callback(self) - - self = ref.__new__(cls, obj, _cb) - self._func_ref = ref(func, _cb) - self._meth_type = type(meth) - self._alive = True - self_wr = ref(self) - return self - - def __call__(self): - obj = super(WeakMethod, self).__call__() - func = self._func_ref() - if obj is None or func is None: - return None - return self._meth_type(func, obj) - - def __eq__(self, other): - if isinstance(other, WeakMethod): - if not self._alive or not other._alive: - return self is other - return ref.__eq__(self, other) and self._func_ref == other._func_ref - return NotImplemented - - def __ne__(self, other): - if isinstance(other, WeakMethod): - if not self._alive or not other._alive: - return self is not other - return ref.__ne__(self, other) or self._func_ref != other._func_ref - return NotImplemented - - __hash__ = ref.__hash__ diff --git a/source/ftrack_api/accessor/base.py b/source/ftrack_api/accessor/base.py index 32085a67..50938191 100644 --- a/source/ftrack_api/accessor/base.py +++ b/source/ftrack_api/accessor/base.py @@ -1,14 +1,12 @@ # :coding: utf-8 # :copyright: Copyright (c) 2013 ftrack -from builtins import object import abc import ftrack_api.exception -from future.utils import with_metaclass -class Accessor(with_metaclass(abc.ABCMeta, object)): +class Accessor(metaclass=abc.ABCMeta): """Provide data access to a location. A location represents a specific storage, but access to that storage may diff --git a/source/ftrack_api/attribute.py b/source/ftrack_api/attribute.py index 4a8fdbab..0a9b158e 100644 --- a/source/ftrack_api/attribute.py +++ b/source/ftrack_api/attribute.py @@ -4,8 +4,7 @@ from __future__ import absolute_import from builtins import object -import collections -from six.moves import collections_abc +import collections.abc import copy import logging import functools @@ -538,7 +537,7 @@ def _adapt_to_collection(self, entity, value): value, self.creator, self.key_attribute, self.value_attribute ) - elif isinstance(value, collections_abc.Mapping): + elif isinstance(value, collections.abc.Mapping): # Convert mapping. # TODO: When backend model improves, revisit this logic. # First get existing value and delete all references. This is @@ -612,7 +611,7 @@ def _adapt_to_collection(self, entity, value): value = ftrack_api.collection.CustomAttributeCollectionProxy(value) - elif isinstance(value, collections_abc.Mapping): + elif isinstance(value, collections.abc.Mapping): # Convert mapping. # TODO: When backend model improves, revisit this logic. # First get existing value and delete all references. This is diff --git a/source/ftrack_api/cache.py b/source/ftrack_api/cache.py index 2ea7ebe4..96ea85b9 100644 --- a/source/ftrack_api/cache.py +++ b/source/ftrack_api/cache.py @@ -16,42 +16,24 @@ """ from builtins import str -from six import string_types from builtins import object -from six.moves import collections_abc import functools import abc +import collections.abc import copy import inspect import re -try: - # Python 2.x - import anydbm -except ImportError: - import dbm as anydbm - - +import pickle import contextlib -from future.utils import with_metaclass - -try: - try: - import _pickle as pickle - except: - import six - from six.moves import cPickle as pickle -except: - try: - import cPickle as pickle - except: - import pickle + +import dbm as anydbm import ftrack_api.inspection import ftrack_api.symbol -class Cache(with_metaclass(abc.ABCMeta, object)): +class Cache(metaclass=abc.ABCMeta): """Cache interface. Derive from this to define concrete cache implementations. A cache is @@ -378,7 +360,7 @@ def set(self, key, value): super(SerialisedCache, self).set(key, value) -class KeyMaker(with_metaclass(abc.ABCMeta, object)): +class KeyMaker(metaclass=abc.ABCMeta): """Generate unique keys.""" def __init__(self): @@ -454,11 +436,11 @@ def __key(self, item): # TODO: Consider using a more robust and comprehensive solution such as # dill (https://github.com/uqfoundation/dill). - if isinstance(item, collections_abc.Iterable): - if isinstance(item, string_types): + if isinstance(item, collections.abc.Iterable): + if isinstance(item, str): return pickle.dumps(item, pickle_protocol) - if isinstance(item, collections_abc.Mapping): + if isinstance(item, collections.abc.Mapping): contents = self.item_separator.join( [ ( diff --git a/source/ftrack_api/collection.py b/source/ftrack_api/collection.py index 1e77f906..173827b9 100644 --- a/source/ftrack_api/collection.py +++ b/source/ftrack_api/collection.py @@ -7,8 +7,7 @@ from builtins import str import logging -import collections -from six.moves import collections_abc +import collections.abc import copy import ftrack_api.exception @@ -19,7 +18,7 @@ from ftrack_api.logging import LazyLogMessage as L -class Collection(collections_abc.MutableSequence): +class Collection(collections.abc.MutableSequence): """A collection of entities.""" def __init__(self, entity, attribute, mutable=True, data=None): @@ -152,7 +151,7 @@ def __ne__(self, other): return not self == other -class MappedCollectionProxy(collections_abc.MutableMapping): +class MappedCollectionProxy(collections.abc.MutableMapping): """Common base class for mapped collection of entities.""" def __init__(self, collection): diff --git a/source/ftrack_api/data.py b/source/ftrack_api/data.py index 2f9a3518..8d05d166 100644 --- a/source/ftrack_api/data.py +++ b/source/ftrack_api/data.py @@ -5,10 +5,9 @@ import os from abc import ABCMeta, abstractmethod import tempfile -from future.utils import with_metaclass -class Data(with_metaclass(ABCMeta, object)): +class Data(metaclass=ABCMeta): """File-like object for manipulating data.""" def __init__(self): diff --git a/source/ftrack_api/entity/base.py b/source/ftrack_api/entity/base.py index cf8fa4b7..e0e4e9ee 100644 --- a/source/ftrack_api/entity/base.py +++ b/source/ftrack_api/entity/base.py @@ -5,8 +5,7 @@ from builtins import str import abc -import collections -from six.moves import collections_abc +import collections.abc import logging import ftrack_api.symbol @@ -15,7 +14,6 @@ import ftrack_api.exception import ftrack_api.operation from ftrack_api.logging import LazyLogMessage as L -from future.utils import with_metaclass class _EntityBase(object): @@ -39,9 +37,7 @@ def __repr__(self): class Entity( - with_metaclass( - DynamicEntityTypeMetaclass, _EntityBase, collections_abc.MutableMapping - ) + _EntityBase, collections.abc.MutableMapping, metaclass=DynamicEntityTypeMetaclass ): """Base class for all entities.""" diff --git a/source/ftrack_api/entity/factory.py b/source/ftrack_api/entity/factory.py index cda4f0d5..195ebd5d 100644 --- a/source/ftrack_api/entity/factory.py +++ b/source/ftrack_api/entity/factory.py @@ -151,10 +151,8 @@ def create(self, schema, bases=None): class_namespace["primary_key_attributes"] = schema["primary_key"][:] class_namespace["default_projections"] = default_projections - from future.utils import native_str - cls = type( - native_str(class_name), # type doesn't accept unicode. + str(class_name), # type doesn't accept unicode. tuple(class_bases), class_namespace, ) diff --git a/source/ftrack_api/entity/location.py b/source/ftrack_api/entity/location.py index 418e345b..3a575fb0 100644 --- a/source/ftrack_api/entity/location.py +++ b/source/ftrack_api/entity/location.py @@ -2,10 +2,7 @@ # :copyright: Copyright (c) 2015 ftrack from builtins import zip -from six import string_types -from builtins import object -import collections -from six.moves import collections_abc +import collections.abc import functools import ftrack_api.entity.base @@ -15,15 +12,6 @@ import ftrack_api.inspection from ftrack_api.logging import LazyLogMessage as L -from future.utils import with_metaclass - - -MixinBaseClass = with_metaclass( - ftrack_api.entity.base.DynamicEntityTypeMetaclass, - ftrack_api.entity.base._EntityBase, - collections_abc.MutableMapping, -) - class Location(ftrack_api.entity.base.Entity): """Represent storage for components.""" @@ -129,8 +117,8 @@ def add_components(self, components, sources, recursive=True, _depth=0): issues and any transferred data under the 'transferred' detail key. """ - if isinstance(sources, string_types) or not isinstance( - sources, collections_abc.Sequence + if isinstance(sources, str) or not isinstance( + sources, collections.abc.Sequence ): sources = [sources] @@ -590,7 +578,11 @@ def get_url(self, component): return self.accessor.get_url(resource_identifier) -class MemoryLocationMixin(MixinBaseClass): +class MemoryLocationMixin( + ftrack_api.entity.base._EntityBase, + collections.abc.MutableMapping, + metaclass=ftrack_api.entity.base.DynamicEntityTypeMetaclass, +): """Represent storage for components. Unlike a standard location, only store metadata for components in this @@ -652,7 +644,11 @@ def _get_resource_identifiers(self, components): return resource_identifiers -class UnmanagedLocationMixin(MixinBaseClass): +class UnmanagedLocationMixin( + ftrack_api.entity.base._EntityBase, + collections.abc.MutableMapping, + metaclass=ftrack_api.entity.base.DynamicEntityTypeMetaclass, +): """Location that does not manage data.""" def _add_data(self, component, resource_identifier, source): @@ -687,7 +683,11 @@ def _get_context(self, component, source): return context -class ServerLocationMixin(MixinBaseClass): +class ServerLocationMixin( + ftrack_api.entity.base._EntityBase, + collections.abc.MutableMapping, + metaclass=ftrack_api.entity.base.DynamicEntityTypeMetaclass, +): """Location representing ftrack server. Adds convenience methods to location, specific to ftrack server. diff --git a/source/ftrack_api/event/base.py b/source/ftrack_api/event/base.py index 733b8ca1..fd37cd9e 100644 --- a/source/ftrack_api/event/base.py +++ b/source/ftrack_api/event/base.py @@ -3,10 +3,10 @@ from builtins import str import uuid -from six.moves import collections_abc +import collections.abc -class Event(collections_abc.MutableMapping): +class Event(collections.abc.MutableMapping): """Represent a single event.""" def __init__( diff --git a/source/ftrack_api/event/expression.py b/source/ftrack_api/event/expression.py index 05ca47bc..6c318ba6 100644 --- a/source/ftrack_api/event/expression.py +++ b/source/ftrack_api/event/expression.py @@ -2,7 +2,6 @@ # :copyright: Copyright (c) 2014 ftrack from builtins import map -from six import string_types from builtins import object from operator import eq, ne, ge, le, gt, lt @@ -266,7 +265,7 @@ def match(self, candidate): if ( self._operator is eq - and isinstance(self._value, string_types) + and isinstance(self._value, str) and self._value[-1] == self._wildcard ): return self._value[:-1] in value diff --git a/source/ftrack_api/event/hub.py b/source/ftrack_api/event/hub.py index 937452a9..4e870677 100644 --- a/source/ftrack_api/event/hub.py +++ b/source/ftrack_api/event/hub.py @@ -6,8 +6,7 @@ from builtins import str from builtins import range from builtins import object -import collections -from six.moves import collections_abc +import collections.abc import urllib.parse import threading import queue as queue @@ -18,7 +17,6 @@ import functools import json import socket -import warnings import ssl import requests @@ -129,7 +127,7 @@ def __init__(self, server_url, api_user, api_key, headers=None, cookies=None): def _validate_mapping(mapping): """Validate mapping is a mapping type and return as dict.""" - if not isinstance(mapping, collections_abc.Mapping): + if not isinstance(mapping, collections.abc.Mapping): raise TypeError("Expected mapping, got {0!r}.".format(mapping)) return dict(mapping) @@ -1014,7 +1012,7 @@ def _handle_packet(self, code, packet_identifier, path, data): if len(args) == 1: event_payload = args[0] - if isinstance(event_payload, collections_abc.Mapping): + if isinstance(event_payload, collections.abc.Mapping): try: event = ftrack_api.event.base.Event(**event_payload) except Exception: @@ -1072,7 +1070,7 @@ def _decode(self, string): def _decode_object_hook(self, item): """Return *item* transformed.""" - if isinstance(item, collections_abc.Mapping): + if isinstance(item, collections.abc.Mapping): if "inReplyToEvent" in item: item["in_reply_to_event"] = item.pop("inReplyToEvent") diff --git a/source/ftrack_api/formatter.py b/source/ftrack_api/formatter.py index 85a36a6b..682af3b4 100644 --- a/source/ftrack_api/formatter.py +++ b/source/ftrack_api/formatter.py @@ -76,15 +76,17 @@ def format( formatters.setdefault( "header", - lambda text: "\x1b[1m\x1b[44m\x1b[97m{}\x1b[0m\033[0m".format(text) - if _can_do_colors() - else text, + lambda text: ( + "\x1b[1m\x1b[44m\x1b[97m{}\x1b[0m\033[0m".format(text) + if _can_do_colors() + else text + ), ) formatters.setdefault( "label", - lambda text: "\x1b[1m\x1b[34m{}\x1b[0m\033[0m".format(text) - if _can_do_colors() - else text, + lambda text: ( + "\x1b[1m\x1b[34m{}\x1b[0m\033[0m".format(text) if _can_do_colors() else text + ), ) # Determine indents. diff --git a/source/ftrack_api/inspection.py b/source/ftrack_api/inspection.py index a9cd8d1e..d1255c20 100644 --- a/source/ftrack_api/inspection.py +++ b/source/ftrack_api/inspection.py @@ -2,7 +2,6 @@ # :copyright: Copyright (c) 2015 ftrack from builtins import str -from future.utils import native_str import collections import ftrack_api.symbol @@ -32,7 +31,7 @@ def primary_key(entity): ) # todo: Compatiblity fix, review for better implementation. - primary_key[native_str(name)] = native_str(value) + primary_key[str(name)] = str(value) return primary_key diff --git a/source/ftrack_api/plugin.py b/source/ftrack_api/plugin.py index 2a28af7b..6e05e8be 100644 --- a/source/ftrack_api/plugin.py +++ b/source/ftrack_api/plugin.py @@ -4,69 +4,28 @@ from __future__ import absolute_import import logging -import collections import os import uuid import traceback -try: - from inspect import getfullargspec - -except ImportError: - # getargspec is deprecated in version 3.0. convert `ArgSpec` to a named - # tuple `FullArgSpec`. We only rely on the values of varargs and varkw. - - # Implemented with "https://github.com/tensorflow/tensorflow/blob/3d69fd003d4acef0ea5663a4794c1e9a4f6ec998/tensorflow/python/util/tf_inspect.py#L34" - # as reference. - import inspect - - FullArgSpec = collections.namedtuple( - "FullArgSpec", - [ - "args", - "varargs", - "varkw", - "defaults", - "kwonlyargs", - "kwonlydefaults", - "annotations", - ], - ) - - def getfullargspec(func): - """a python 2 version of `getfullargspec`.""" - spec = inspect.getargspec(func) +import importlib.util +import importlib.machinery - return FullArgSpec( - args=spec.args, - varargs=spec.varargs, - varkw=spec.keywords, - defaults=spec.defaults, - kwonlyargs=[], - kwonlydefaults=None, - annotations={}, - ) +import inspect -try: - from imp import load_source -except ImportError: - # The imp module is deprecated in version 3.12. Use importlib instead. - import importlib.util - import importlib.machinery +def load_source(modname, filename): + # https://docs.python.org/3/whatsnew/3.12.html#imp - def load_source(modname, filename): - # https://docs.python.org/3/whatsnew/3.12.html#imp - - loader = importlib.machinery.SourceFileLoader(modname, filename) - module = importlib.util.module_from_spec( - importlib.util.spec_from_file_location(modname, filename, loader=loader) - ) + loader = importlib.machinery.SourceFileLoader(modname, filename) + module = importlib.util.module_from_spec( + importlib.util.spec_from_file_location(modname, filename, loader=loader) + ) - loader.exec_module(module) + loader.exec_module(module) - return module + return module def discover(paths, positional_arguments=None, keyword_arguments=None): @@ -126,7 +85,7 @@ def discover(paths, positional_arguments=None, keyword_arguments=None): else: # Attempt to only pass arguments that are accepted by the # register function. - specification = getfullargspec(module.register) + specification = inspect.getfullargspec(module.register) selected_positional_arguments = positional_arguments selected_keyword_arguments = keyword_arguments diff --git a/source/ftrack_api/query.py b/source/ftrack_api/query.py index 62c7dd5b..2443cf08 100644 --- a/source/ftrack_api/query.py +++ b/source/ftrack_api/query.py @@ -2,12 +2,12 @@ # :copyright: Copyright (c) 2014 ftrack import re -from six.moves import collections_abc +import collections.abc import ftrack_api.exception -class QueryResult(collections_abc.Sequence): +class QueryResult(collections.abc.Sequence): """Results from a query.""" OFFSET_EXPRESSION = re.compile("(?Poffset (?P\d+))") diff --git a/source/ftrack_api/session.py b/source/ftrack_api/session.py index e0d3fa10..ff6f2988 100644 --- a/source/ftrack_api/session.py +++ b/source/ftrack_api/session.py @@ -7,12 +7,10 @@ from builtins import zip from builtins import map from builtins import str -from six import string_types from builtins import object import json import logging -import collections -from six.moves import collections_abc +import collections.abc import datetime import os import getpass @@ -54,10 +52,7 @@ import ftrack_api.logging from ftrack_api.logging import LazyLogMessage as L -try: - from weakref import WeakMethod -except ImportError: - from ftrack_api._weakref import WeakMethod +from weakref import WeakMethod class SessionAuthentication(requests.auth.AuthBase): @@ -254,12 +249,12 @@ def __init__( self._request = requests.Session() if cookies: - if not isinstance(cookies, collections_abc.Mapping): + if not isinstance(cookies, collections.abc.Mapping): raise TypeError("The cookies argument is required to be a mapping.") self._request.cookies.update(cookies) if headers: - if not isinstance(headers, collections_abc.Mapping): + if not isinstance(headers, collections.abc.Mapping): raise TypeError("The headers argument is required to be a mapping.") headers = dict(headers) @@ -720,7 +715,7 @@ def ensure(self, entity_type, data, identifying_keys=None): for identifying_key in identifying_keys: value = data[identifying_key] - if isinstance(value, string_types): + if isinstance(value, str): value = '"{0}"'.format(value) elif isinstance(value, (arrow.Arrow, datetime.datetime, datetime.date)): @@ -781,7 +776,7 @@ def get(self, entity_type, entity_key): self.logger.debug(L("Get {0} with key {1}", entity_type, entity_key)) primary_key_definition = self.types[entity_type].primary_key_attributes - if isinstance(entity_key, string_types): + if isinstance(entity_key, str): entity_key = [entity_key] if len(entity_key) != len(primary_key_definition): @@ -1802,7 +1797,7 @@ def decode(self, string): def _decode(self, item): """Return *item* transformed into appropriate representation.""" - if isinstance(item, collections_abc.Mapping): + if isinstance(item, collections.abc.Mapping): if "__type__" in item: if item["__type__"] == "datetime": item = arrow.get(item["value"]) @@ -2204,7 +2199,7 @@ def encode_media(self, media, version_id=None, keep_original="auto"): is a FileComponent, and deleted if it is a file path. You can specify True or False to change this behavior. """ - if isinstance(media, string_types): + if isinstance(media, str): # Media is a path to a file. server_location = self.get("Location", ftrack_api.symbol.SERVER_LOCATION_ID) if keep_original == "auto": @@ -2412,7 +2407,7 @@ def __exit__(self, exception_type, exception_value, traceback): self._session.record_operations = self._current_record_operations -class OperationPayload(collections_abc.MutableMapping): +class OperationPayload(collections.abc.MutableMapping): """Represent operation payload.""" def __init__(self, *args, **kwargs): diff --git a/source/ftrack_api/structure/base.py b/source/ftrack_api/structure/base.py index bf4a4a4f..266abd56 100644 --- a/source/ftrack_api/structure/base.py +++ b/source/ftrack_api/structure/base.py @@ -1,12 +1,10 @@ # :coding: utf-8 # :copyright: Copyright (c) 2014 ftrack -from builtins import object from abc import ABCMeta, abstractmethod -from future.utils import with_metaclass -class Structure(with_metaclass(ABCMeta, object)): +class Structure(metaclass=ABCMeta): """Structure plugin interface. A structure plugin should compute appropriate paths for data. diff --git a/test/unit/entity/test_user.py b/test/unit/entity/test_user.py index 9a3ab0c3..8e6d6c39 100644 --- a/test/unit/entity/test_user.py +++ b/test/unit/entity/test_user.py @@ -1,8 +1,6 @@ # :coding: utf-8 # :copyright: Copyright (c) 2016 ftrack -from past.builtins import long - def test_force_start_timer(new_user, task): """Successfully force starting a timer when another timer is running.""" @@ -31,7 +29,7 @@ def test_timer_creates_timelog(new_user, task, unique_name): assert timelog["name"] == unique_name assert timelog["comment"] == comment assert timelog["start"] == timer_start - assert isinstance(timelog["duration"], (int, long, float)) + assert isinstance(timelog["duration"], (int, float)) assert timelog["duration"] < 60