diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..16dd819f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[coverage:run] +omit = + substanced/*/evolve.py + substanced/evolution/evolve*.py + substanced/scripts/*.py + +[coverage:report] +show_missing = true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..1c677242 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,66 @@ +name: Build and test + +on: + push: + branches: + - master + tags: + pull_request: + +jobs: + test: + strategy: + matrix: + py: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + os: + - "ubuntu-22.04" + architecture: + - x64 + include: + # Only run coverage on ubuntu-22.04, except on pypy3 + - os: "ubuntu-22.04" + pytest-args: "--cov" + + name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.py }} + architecture: ${{ matrix.architecture }} + - run: pip install tox + - name: Running tox + run: tox -e py -- ${{ matrix.pytest-args }} + + coverage: + runs-on: ubuntu-22.04 + name: Validate coverage + steps: + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + architecture: x64 + - run: pip install tox + - run: tox -e cover + + docs: + runs-on: ubuntu-22.04 + name: Build the documentation + steps: + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + architecture: x64 + - run: pip install tox + - run: tox -e docs diff --git a/CHANGES.txt b/CHANGES.txt index bef3ca55..b75afa82 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,16 @@ 1.0b1 (unreleased) ================== +- Drop old Python versions (< 3.8), add newer ones (up to 3.12). + - Remove all the "straddle" compatibility stuff FBO Python 2.7 + - Pin pyramid < 2.0dev, until it can be reviewed in depth. + - Fix deprecation warnings from pyramid. + - Silence deprecation warnings related to pkg_resources. + - Make ``py.test`` the testrunner (to support the newer Python versions). + - Replace ``cryptacular`` with plain ``bcrypt`` (the ``enscons`` builder + that ``cryptacular`` needs barfs on Python 3.12). + See: https://github.com/Pylons/substanced/pull/307 + - Override ``serialize`` for ``ReferenceIdSchemaNode``. See https://github.com/Pylons/substanced/pull/311 diff --git a/TODO.txt b/TODO.txt index ee151a0b..ca0683fb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -109,7 +109,7 @@ Probably Bad Ideas how contained object can be acted upon? E.g.:: def __viewable__(self, context, request): - return has_permission('sdi.view', context, request) + return request.has_permission('sdi.view', context) Made Irrelevant diff --git a/docs/locking.rst b/docs/locking.rst index cf147b2e..c9ce888a 100644 --- a/docs/locking.rst +++ b/docs/locking.rst @@ -17,9 +17,8 @@ To lock a resource: .. code-block:: python from substanced.locking import lock_resource - from pyramid.security import has_permission - if has_permission('sdi.lock', someresource, request): + if request.has_permission('sdi.lock', someresource): lock_resource(someresource, request.user, timeout=3600) If the resource is already locked by the owner supplied as ``owner_or_ownerid`` @@ -47,9 +46,8 @@ To unlock a resource: .. code-block:: python from substanced.locking import unlock_resource - from pyramid.security import has_permission - if has_permission('sdi.lock', someresource, request): + if request.has_permission('sdi.lock', someresource): unlock_resource(someresource, request.user) If the resource is already locked by a user other than the owner supplied as @@ -73,9 +71,8 @@ To unlock a resource using an explicit lock token: .. code-block:: python from substanced.locking import unlock_token - from pyramid.security import has_permission - if has_permission('sdi.lock', someresource, request): + if request.has_permission('sdi.lock', someresource): unlock_token(someresource, token, request.user) If the lock identified by ``token`` belongs to a user other than the owner diff --git a/pytest.ini b/pytest.ini index 7aca27be..8e2c1f9a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,6 @@ addopts = -l --strict norecursedirs = lib include .tox .git python_files = test_*.py tests.py +filterwarnings = + ignore::DeprecationWarning:pkg_resources + ignore::DeprecationWarning:zodburi diff --git a/setup.cfg b/setup.cfg index ec5bdfeb..6565c463 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,6 @@ [easy_install] zip_ok = false -[nosetests] -match=^test -where=substanced -nocapture=1 -cover-package=substanced -cover-erase=1 - [aliases] dev = develop easy_install substanced[testing] docs = develop easy_install substanced[docs] diff --git a/setup.py b/setup.py index 11c4ef5b..a50dca0a 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ README = CHANGES = '' install_requires = [ - 'pyramid>=1.5dev', # route_name argument to resource_url + 'pyramid>=1.5dev,<2.0dev', # route_name argument to resource_url 'ZODB', 'hypatia>=0.2', # query objects have intersection/union methods 'venusian>=1.0a3', # pyramid wants this too (prefer_finals...) @@ -34,7 +34,7 @@ 'pyramid_zodbconn>=0.6', # connection opened/closed events 'pyramid_chameleon', 'pyramid_mailer', - 'cryptacular', + 'bcrypt', 'python-magic', 'PyYAML', 'zope.copy', @@ -70,13 +70,12 @@ classifiers=[ "Intended Audience :: Developers", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "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", "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP :: WSGI", diff --git a/substanced/_compat.py b/substanced/_compat.py deleted file mode 100644 index d38096d1..00000000 --- a/substanced/_compat.py +++ /dev/null @@ -1,57 +0,0 @@ - -try: - STRING_TYPES = (str, unicode) -except NameError: #pragma NO COVER Python >= 3.0 - STRING_TYPES = (str,) - -try: - u = unicode -except NameError: #pragma NO COVER Python >= 3.0 - TEXT = str - def u(x, encoding='ascii'): - if isinstance(x, str): - return x - if isinstance(x, bytes): - return x.decode(encoding) - b = bytes -else: #pragma NO COVER Python < 3.0 - TEXT = unicode - b = str - -try: - INT_TYPES = (int, long) -except NameError: #pragma NO COVER Python >= 3.0 - INT_TYPES = (int,) - -try: # pragma: no cover Python < 3.0 - from base64 import decodebytes - from base64 import encodebytes -except ImportError: #pragma NO COVER - from base64 import decodestring as decodebytes - from base64 import encodestring as encodebytes - -try: - from urllib.parse import parse_qsl -except ImportError: #pragma NO COVER - from cgi import parse_qsl - -try: - from urllib.parse import urlsplit -except ImportError: #pragma NO COVER - from urlparse import urlsplit - from urlparse import urlunsplit -else: #pragma NO COVER - from urllib.parse import urlunsplit - -import string -try: - _LETTERS = string.letters -except AttributeError: #pragma NO COVER - _LETTERS = string.ascii_letters -del string - -try: - from html import escape # py3 -except ImportError: #pragma NO COVER - from cgi import escape - diff --git a/substanced/audit/__init__.py b/substanced/audit/__init__.py index e1c4a08c..320193e0 100644 --- a/substanced/audit/__init__.py +++ b/substanced/audit/__init__.py @@ -4,7 +4,7 @@ from persistent import Persistent from ZODB.POSException import ConflictError -from pyramid.compat import is_nonstr_iter +from ..util import is_nonstr_iter class LayerFull(Exception): pass diff --git a/substanced/audit/views.py b/substanced/audit/views.py index 1bf99910..f0c94427 100644 --- a/substanced/audit/views.py +++ b/substanced/audit/views.py @@ -3,7 +3,6 @@ from pyramid.view import view_defaults from pyramid.httpexceptions import HTTPPreconditionFailed -from pyramid.compat import PY3, text_type from substanced.sdi import mgmt_view, RIGHT from substanced.util import ( Batch, @@ -99,7 +98,7 @@ def auditstream_sse(self): oids = [get_oid(self.context)] _gen, _idx = map(int, last_event_id.split('-', 1)) events = log.newer(_gen, _idx, oids=oids) - msg = text_type('') + msg = '' for gen, idx, event in events: event_id = '%s-%s' % (gen, idx) message = compose_message(event_id, event.name, event.payload) @@ -118,8 +117,4 @@ def compose_message(eventid, name=None, payload=''): msg += 'retry: 10000\n' msg += 'data: %s\n' % payload msg += '\n' - if PY3: # pragma: no cover - return msg - else: # pragma: no cover - return msg.decode('utf-8') - + return msg diff --git a/substanced/catalog/__init__.py b/substanced/catalog/__init__.py index 397b4e52..a635a77f 100644 --- a/substanced/catalog/__init__.py +++ b/substanced/catalog/__init__.py @@ -32,8 +32,6 @@ from ..objectmap import find_objectmap from ..stats import statsd_timer from ..util import get_oid -from .._compat import INT_TYPES -from .._compat import u from .factories import ( IndexFactory, @@ -70,7 +68,7 @@ logger = logging.getLogger(__name__) # API -_SLASH = u('/') +_SLASH = '/' _marker = object() @@ -174,7 +172,7 @@ def unindex_resource(self, resource_or_oid, action_mode=None): which explicitly indicates that you'd like to use the index's action_mode value.""" oid = get_oid(resource_or_oid, resource_or_oid) - if not isinstance(oid, INT_TYPES): + if not isinstance(oid, int): raise ValueError( 'resource_or_oid must be a resource object with an __oid__ ' 'attribute or an integer oid' diff --git a/substanced/catalog/indexes.py b/substanced/catalog/indexes.py index 3752521f..30916ce4 100644 --- a/substanced/catalog/indexes.py +++ b/substanced/catalog/indexes.py @@ -1,3 +1,5 @@ +from urllib.parse import unquote as url_unquote + import colander import deform.widget import re @@ -11,12 +13,7 @@ import hypatia.text import hypatia.util from persistent import Persistent -from pyramid.compat import ( - url_unquote_text, - is_nonstr_iter, - ) from pyramid.settings import asbool -from pyramid.security import effective_principals from pyramid.traversal import resource_path_tuple from pyramid.interfaces import IRequest from zope.interface import implementer @@ -31,9 +28,7 @@ from ..property import PropertySheet from ..schema import Schema from ..stats import statsd_timer -from .._compat import STRING_TYPES -from .._compat import INT_TYPES -from .._compat import u +from ..util import is_nonstr_iter from .discriminators import dummy_discriminator from .util import oid_from_resource @@ -41,8 +36,8 @@ from . import deferred PATH_WITH_OPTIONS = re.compile(r'\[(.+?)\](.+?)$') -_BLANK = u('') -_SLASH = u('/') +_BLANK = '' +_SLASH = '/' _marker = object() @@ -107,7 +102,7 @@ def reindex_resource(self, resource, oid=None, action_mode=None): self.add_action(action) def unindex_resource(self, resource_or_oid, action_mode=None): - if isinstance(resource_or_oid, INT_TYPES): + if isinstance(resource_or_oid, int): oid = resource_or_oid else: oid = oid_from_resource(resource_or_oid) @@ -243,7 +238,7 @@ def _parse_path_str(self, path_str): if not path.startswith('/'): raise ValueError('Path must start with a slash') - tmp = [x for x in url_unquote_text(path).split(_SLASH) if x] + tmp = [x for x in url_unquote(path).split(_SLASH) if x] path_tuple = (_BLANK,) + tuple(tmp) return path_tuple, depth, include_origin @@ -253,7 +248,7 @@ def _parse_path(self, obj_or_path): path_tuple = obj_or_path if hasattr(obj_or_path, '__parent__'): path_tuple = resource_path_tuple(obj_or_path) - elif isinstance(obj_or_path, STRING_TYPES): + elif isinstance(obj_or_path, str): path_tuple, depth, include_origin = self._parse_path_str( obj_or_path) elif not isinstance(obj_or_path, tuple): @@ -422,7 +417,7 @@ def allows(self, principals, permission): ``permission`` must be a permission name. """ if IRequest.providedBy(principals): - principals = effective_principals(principals) + principals = principals.effective_principals elif not is_nonstr_iter(principals): principals = (principals,) return AllowsComparator(self, (principals, permission)) diff --git a/substanced/catalog/subscribers.py b/substanced/catalog/subscribers.py index 4595eab1..e194455a 100644 --- a/substanced/catalog/subscribers.py +++ b/substanced/catalog/subscribers.py @@ -144,7 +144,7 @@ def on_startup(event): # evolve because autosync is on and causes errors, cant autosync # because evolve steps havent been run), so we avoid doing any # sync if there are unfinished evolve steps - logger.warn( + logger.warning( 'Cannot autosync/autoreindex catalog due to unfinished evolve ' 'steps' ) diff --git a/substanced/catalog/tests/test_catalog.py b/substanced/catalog/tests/test_catalog.py index 60fb3b62..131867bb 100644 --- a/substanced/catalog/tests/test_catalog.py +++ b/substanced/catalog/tests/test_catalog.py @@ -10,10 +10,9 @@ from hypatia.interfaces import IIndex -from ..._compat import u -_BLANK = u('') -_A = u('a') -_B = u('b') +_BLANK = '' +_A = 'a' +_B = 'b' def _makeSite(**kw): from ...interfaces import IFolder diff --git a/substanced/catalog/tests/test_indexes.py b/substanced/catalog/tests/test_indexes.py index f74fbe72..b4b106dc 100644 --- a/substanced/catalog/tests/test_indexes.py +++ b/substanced/catalog/tests/test_indexes.py @@ -3,10 +3,9 @@ import BTrees -from ..._compat import u -_BLANK = u('') -_A = u('a') -_ABC = u('abc') +_BLANK = '' +_A = 'a' +_ABC = 'abc' def _makeSite(**kw): from ...interfaces import IFolder diff --git a/substanced/catalog/util.py b/substanced/catalog/util.py index caa78fa5..e484c50a 100644 --- a/substanced/catalog/util.py +++ b/substanced/catalog/util.py @@ -1,9 +1,8 @@ from ..util import get_oid -from .._compat import INT_TYPES def oid_from_resource(resource): oid = get_oid(resource, None) - if not isinstance(oid, INT_TYPES): + if not isinstance(oid, int): raise ValueError( 'Resource must be an object with an integer __oid__ attribute' ) diff --git a/substanced/catalog/views/indexing.py b/substanced/catalog/views/indexing.py index 8e803463..79e39869 100644 --- a/substanced/catalog/views/indexing.py +++ b/substanced/catalog/views/indexing.py @@ -1,6 +1,6 @@ from pyramid.view import view_defaults from pyramid.httpexceptions import HTTPFound -from pyramid.session import check_csrf_token +from pyramid.csrf import check_csrf_token from substanced.interfaces import MODE_IMMEDIATE from substanced.util import ( diff --git a/substanced/content/__init__.py b/substanced/content/__init__.py index 2499700c..e8594614 100644 --- a/substanced/content/__init__.py +++ b/substanced/content/__init__.py @@ -1,6 +1,5 @@ import inspect -from pyramid.compat import is_nonstr_iter from pyramid.location import lineage import venusian @@ -9,8 +8,8 @@ get_dotted_name, set_oid, get_factory_type, + is_nonstr_iter, ) -from .._compat import STRING_TYPES _marker = object() @@ -57,7 +56,7 @@ def create(self, content_type, *arg, **kw): if not is_nonstr_iter(aftercreate): aftercreate = [aftercreate] for callback in aftercreate: - if isinstance(callback, STRING_TYPES): + if isinstance(callback, str): callback = getattr(inst, callback) callback(inst, self.registry) self.registry.subscribers( diff --git a/substanced/content/tests.py b/substanced/content/test_it.py similarity index 98% rename from substanced/content/tests.py rename to substanced/content/test_it.py index 005735c7..2368712b 100644 --- a/substanced/content/tests.py +++ b/substanced/content/test_it.py @@ -218,7 +218,7 @@ def factory(): return dummy ft = config.actions[0][0][0] self.assertEqual( ft, - ('sd-factory-type', 'substanced.content.tests.factory') + ('sd-factory-type', 'substanced.content.test_it.factory') ) self.assertEqual( config.actions[1][0][0], @@ -242,7 +242,7 @@ class Foo(object): ft = config.actions[0][0][0] self.assertEqual( ft, - ('sd-factory-type', 'substanced.content.tests.Foo') + ('sd-factory-type', 'substanced.content.test_it.Foo') ) self.assertEqual( config.actions[1][0][0], @@ -292,7 +292,7 @@ class Foo(object): pass factory_type, factory = self._callFUT(Foo, None) self.assertTrue(factory is Foo) - self.assertEqual(factory_type, 'substanced.content.tests.Foo') + self.assertEqual(factory_type, 'substanced.content.test_it.Foo') def test_content_factory_isclass_factory_type_is_supplied(self): class Foo(object): @@ -316,7 +316,7 @@ def ctor(): self.assertTrue(factory.__factory__ is ctor) ob = factory() self.assertTrue(ob is foo) - self.assertEqual(ob.__factory_type__, 'substanced.content.tests.ctor') + self.assertEqual(ob.__factory_type__, 'substanced.content.test_it.ctor') def test_content_factory_isfunction_factory_type_is_supplied(self): class Foo(object): @@ -415,7 +415,7 @@ def call_venusian(venusian, context=None): class DummyVenusianInfo(object): scope = 'notaclass' - module = sys.modules['substanced.content.tests'] + module = sys.modules['substanced.content.test_it'] codeinfo = 'codeinfo' class DummyVenusian(object): diff --git a/substanced/dump/__init__.py b/substanced/dump/__init__.py index bbe8fcb9..52ba81f9 100644 --- a/substanced/dump/__init__.py +++ b/substanced/dump/__init__.py @@ -1,3 +1,5 @@ +from base64 import decodebytes +from base64 import encodebytes import logging import os @@ -30,10 +32,6 @@ get_content_type, is_folder, ) -from .._compat import TEXT -from .._compat import u -from .._compat import decodebytes -from .._compat import encodebytes logger = logging.getLogger(__name__) @@ -41,6 +39,11 @@ RESOURCE_FILENAME = 'resource.yaml' RESOURCES_DIRNAME = 'resources' +DUMP_INTERFACE = '!interface' +DUMP_BLOB = '!blob' +DUMP_ALL_PERMISSIONS = '!all_permissions' +DUMP_COLANDER_NULL = '!colander_null' + dotted_name_resolver = DottedNameResolver() class IDumperFactories(Interface): @@ -60,27 +63,28 @@ class SLoader(yaml.Loader): registry['yaml_dumper'] = SDumper def iface_representer(dumper, data): - return dumper.represent_scalar(u('!interface'), get_dotted_name(data)) + return dumper.represent_scalar(DUMP_INTERFACE, get_dotted_name(data)) def iface_constructor(loader, node): return dotted_name_resolver.resolve(node.value) SDumper.add_multi_representer(InterfaceClass, iface_representer) - SLoader.add_constructor(u('!interface'), iface_constructor) + SLoader.add_constructor(DUMP_INTERFACE, iface_constructor) def blob_representer(dumper, data): with data.open('r') as f: data = f.read() encoded = encodebytes(data) u_encoded = encoded.decode('ascii') - return dumper.represent_scalar(u('!blob'), u_encoded) + return dumper.represent_scalar(DUMP_BLOB, u_encoded) + def blob_constructor(loader, node): value = node.value - if isinstance(value, TEXT): + if isinstance(value, str): value = value.encode('ascii') return Blob(decodebytes(value)) SDumper.add_representer(Blob, blob_representer) - SLoader.add_constructor(u('!blob'), blob_constructor) + SLoader.add_constructor(DUMP_BLOB, blob_constructor) def get_dumpers(registry): @@ -395,12 +399,15 @@ def __init__(self, name, registry): self.name = name self.registry = registry self.fn = '%s.yaml' % self.name + def ap_constructor(loader, node): return ALL_PERMISSIONS + def ap_representer(dumper, data): - return dumper.represent_scalar(u('!all_permissions'), '') + return dumper.represent_scalar(DUMP_ALL_PERMISSIONS, '') + registry['yaml_loader'].add_constructor( - u('!all_permissions'), + DUMP_ALL_PERMISSIONS, ap_constructor, ) registry['yaml_dumper'].add_representer( @@ -558,9 +565,9 @@ def __init__(self, name, registry): def cn_constructor(loader, node): return colander.null def cn_representer(dumper, data): - return dumper.represent_scalar(u('!colander_null'), '') + return dumper.represent_scalar(DUMP_COLANDER_NULL, '') registry['yaml_loader'].add_constructor( - u('!colander_null'), + DUMP_COLANDER_NULL, cn_constructor, ) registry['yaml_dumper'].add_representer( diff --git a/substanced/dump/tests.py b/substanced/dump/test_it.py similarity index 98% rename from substanced/dump/tests.py rename to substanced/dump/test_it.py index 21040c54..fe817450 100644 --- a/substanced/dump/tests.py +++ b/substanced/dump/test_it.py @@ -22,7 +22,7 @@ def test_iface_representer(self): encoding='utf-8') self.assertEqual( stream.getvalue(), - b"!interface 'substanced.dump.tests.DummyInterface'\n" + b"!interface 'substanced.dump.test_it.DummyInterface'\n" ) def test_iface_constructor(self): @@ -31,7 +31,7 @@ def test_iface_constructor(self): registry = DummyRegistry(None) self._callFUT(registry) stream = io.BytesIO( - b"!interface 'substanced.dump.tests.DummyInterface'\n" + b"!interface 'substanced.dump.test_it.DummyInterface'\n" ) result = yaml.load(stream, Loader=registry['yaml_loader']) self.assertEqual(result, DummyInterface) @@ -322,16 +322,16 @@ def _makeOne(self): return _ResourceContext() def test_resolve_dotted_name(self): - import substanced.dump.tests + import substanced.dump.test_it inst = self._makeOne() - result = inst.resolve_dotted_name('substanced.dump.tests') - self.assertEqual(result, substanced.dump.tests) + result = inst.resolve_dotted_name('substanced.dump.test_it') + self.assertEqual(result, substanced.dump.test_it) def test_get_dotted_name(self): - import substanced.dump.tests + import substanced.dump.test_it inst = self._makeOne() - result = inst.get_dotted_name(substanced.dump.tests) - self.assertEqual(result, 'substanced.dump.tests') + result = inst.get_dotted_name(substanced.dump.test_it) + self.assertEqual(result, 'substanced.dump.test_it') class Test_ResourceDumpContext(unittest.TestCase): def _makeOne(self, directory, registry, dumpers, verbose, dry_run): @@ -477,7 +477,7 @@ def _makeOne(self, name, registry): def test_init_adds_yaml_stuff(self): from pyramid.security import ALL_PERMISSIONS - from .._compat import u + from . import DUMP_ALL_PERMISSIONS yamlthing = DummyYAMLDumperLoader() registry = {'yaml_loader':yamlthing, 'yaml_dumper':yamlthing} self._makeOne('name', registry) @@ -488,7 +488,7 @@ def test_init_adds_yaml_stuff(self): ) dumper = testing.DummyResource() def represent_scalar(one, two): - self.assertEqual(one, u('!all_permissions')) + self.assertEqual(one, DUMP_ALL_PERMISSIONS) dumper.represent_scalar = represent_scalar yamlthing.representers[0][1](dumper, None) @@ -727,7 +727,7 @@ def _makeOne(self, name, registry): def test_init_adds_yaml_stuff(self): import colander - from .._compat import u + from . import DUMP_COLANDER_NULL yamlthing = DummyYAMLDumperLoader() registry = {'yaml_loader':yamlthing, 'yaml_dumper':yamlthing} self._makeOne('name', registry) @@ -738,7 +738,7 @@ def test_init_adds_yaml_stuff(self): ) dumper = testing.DummyResource() def represent_scalar(one, two): - self.assertEqual(one, u('!colander_null')) + self.assertEqual(one, DUMP_COLANDER_NULL) dumper.represent_scalar = represent_scalar yamlthing.representers[0][1](dumper, None) diff --git a/substanced/editable/tests.py b/substanced/editable/test_it.py similarity index 100% rename from substanced/editable/tests.py rename to substanced/editable/test_it.py diff --git a/substanced/event/tests.py b/substanced/event/test_it.py similarity index 100% rename from substanced/event/tests.py rename to substanced/event/test_it.py diff --git a/substanced/evolution/__init__.py b/substanced/evolution/__init__.py index ff9403d6..efe7c8ee 100644 --- a/substanced/evolution/__init__.py +++ b/substanced/evolution/__init__.py @@ -8,7 +8,6 @@ from ..interfaces import IEvolutionSteps from ..util import get_dotted_name -from .._compat import STRING_TYPES try: # pyramid 1.9 and below @@ -135,9 +134,9 @@ def add_evolution_step(config, func, before=None, after=None, name=None): name = get_dotted_name(func) else: func_desc = func_desc + ' (%s)' % name - if after is not None and not isinstance(after, STRING_TYPES): + if after is not None and not isinstance(after, str): after = get_dotted_name(after) - if before is not None and not isinstance(before, STRING_TYPES): + if before is not None and not isinstance(before, str): before = get_dotted_name(before) discriminator = ('evolution step', name) intr = config.introspectable( diff --git a/substanced/evolution/evolve2.py b/substanced/evolution/evolve2.py index a7325798..b565b287 100644 --- a/substanced/evolution/evolve2.py +++ b/substanced/evolution/evolve2.py @@ -3,19 +3,16 @@ from substanced.util import postorder from substanced.interfaces import PrincipalToACLBearing from substanced.objectmap import find_objectmap -from substanced._compat import INT_TYPES _marker = object() logger = logging.getLogger('evolution') -_TO_APPEND = INT_TYPES + (tuple,) - def _referenceable_principals(acl): result = set() for ace in (acl or ()): principal_id = ace[1] - if isinstance(principal_id, _TO_APPEND): + if isinstance(principal_id, (int, tuple)): result.add(principal_id) return result diff --git a/substanced/evolution/evolve6.py b/substanced/evolution/evolve6.py index 6cac37ff..ef4e47e3 100644 --- a/substanced/evolution/evolve6.py +++ b/substanced/evolution/evolve6.py @@ -4,7 +4,6 @@ import logging from substanced.file import File from substanced.util import get_dotted_name -from pyramid.compat import string_types _marker = object() @@ -21,5 +20,5 @@ def evolve(root, registry): if oids is not None: for oid in oids: f = objectmap.object_for(oid) - if not type(f.mimetype) in string_types: + if not isinstance(f.mimetype, str): f.mimetype = 'application/octet-stream' diff --git a/substanced/evolution/evolve8.py b/substanced/evolution/evolve8.py index 8f3202ae..e7153e73 100644 --- a/substanced/evolution/evolve8.py +++ b/substanced/evolution/evolve8.py @@ -1,7 +1,5 @@ import logging -from pyramid.compat import string_types - from ..util import ( get_oid, is_folder, @@ -33,7 +31,7 @@ def evolve(root, registry): oid_order = () name_order = () if order: - if isinstance(order[0], string_types): + if isinstance(order[0], str): # handle master branch name_order = obj._order oid_order = [] diff --git a/substanced/evolution/tests/test_evolution.py b/substanced/evolution/tests/test_evolution.py index c78181c7..890f518e 100644 --- a/substanced/evolution/tests/test_evolution.py +++ b/substanced/evolution/tests/test_evolution.py @@ -1,4 +1,5 @@ import unittest +import warnings class TestEvolutionManager(unittest.TestCase): def _makeOne(self, context, registry, txn): @@ -141,8 +142,17 @@ def func(context): L = [] inst.out = L.append inst.get_unfinished_steps = lambda *arg: [('name', func)] - inst.evolve(False) + + with warnings.catch_warnings(record=True) as log: + inst.evolve(False) + self.assertEqual(len(L), 1) + assert len(log) == 1 + warned = log[0] + assert warned.category is DeprecationWarning + assert warned.message.args[0].startswith( + "Single argument evolution function" + ) class Test_mark_unfinished_as_finished(unittest.TestCase): diff --git a/substanced/file/__init__.py b/substanced/file/__init__.py index 98422855..fa48a63c 100644 --- a/substanced/file/__init__.py +++ b/substanced/file/__init__.py @@ -12,9 +12,8 @@ from zope.interface import implementer from ..util import get_oid -from .._compat import u -_BLANK = u('') +_BLANK = '' try: import magic @@ -227,8 +226,7 @@ def upload(self, stream, mimetype_hint=None): if use_magic and first: first = False m = magic.Magic(mime=True) - mimetype = m.from_buffer(chunk) - self.mimetype = u(mimetype) + self.mimetype = m.from_buffer(chunk) fp.write(chunk) fp.close() diff --git a/substanced/file/tests/test_init.py b/substanced/file/tests/test_init.py index 461cb066..02092875 100644 --- a/substanced/file/tests/test_init.py +++ b/substanced/file/tests/test_init.py @@ -148,14 +148,12 @@ def test_ctor_no_stream(self): self.assertEqual(inst.mimetype, 'application/octet-stream') def test_ctor_no_title(self): - from substanced._compat import u inst = self._makeOne(None, None) - self.assertEqual(inst.title, u('')) + self.assertEqual(inst.title, '') def test_ctor_with_None_title(self): - from substanced._compat import u inst = self._makeOne(None, None, None) - self.assertEqual(inst.title, u('')) + self.assertEqual(inst.title, '') def test_ctor_with_with_title(self): inst = self._makeOne(None, None, 'abc') diff --git a/substanced/folder/__init__.py b/substanced/folder/__init__.py index bb9a7cfb..b427d416 100644 --- a/substanced/folder/__init__.py +++ b/substanced/folder/__init__.py @@ -7,10 +7,6 @@ Persistent, ) from persistent.interfaces import IPersistent -from pyramid.compat import ( - is_nonstr_iter, - string_types, - ) from pyramid.location import ( lineage, inside, @@ -48,9 +44,8 @@ find_service, find_services, wrap_if_broken, + is_nonstr_iter, ) -from .._compat import STRING_TYPES -from .._compat import u class FolderKeyError(KeyError): @@ -118,8 +113,7 @@ def set_order(self, names, reorderable=None): order_oids = [] for name in names: - assert(isinstance(name, string_types)) - name = u(name) + assert(isinstance(name, str)) oid = get_oid(self[name]) order.append(name) order_oids.append(oid) @@ -173,8 +167,7 @@ def reorder(self, names, before): reorder_oids = [] for name in names: - assert(isinstance(name, string_types)) - name = u(name) + assert(isinstance(name, str)) if not name in order_names: raise FolderKeyError(name) idx = order_names.index(name) @@ -321,8 +314,10 @@ def __getitem__(self, name): object or directly decodeable to Unicode using the system default encoding. """ + if isinstance(name, bytes): + name = name.decode() + with statsd_timer('folder.get'): - name = u(name) return wrap_if_broken(self.data[name]) def get(self, name, default=None): @@ -334,7 +329,6 @@ def get(self, name, default=None): system default encoding. """ with statsd_timer('folder.get'): - name = u(name) return wrap_if_broken(self.data.get(name, default)) def __contains__(self, name): @@ -345,7 +339,6 @@ def __contains__(self, name): If ``name`` is a bytestring object, it must be decodable using the system default encoding. """ - name = u(name) return name in self.data def __setitem__(self, name, other): @@ -393,17 +386,12 @@ def validate_name(self, name, reserved_names=()): the name passed is in the list of ``reserved_names``, raise a :exc:`ValueError`. """ - if not isinstance(name, STRING_TYPES): + if not isinstance(name, str): raise ValueError("Name must be a string rather than a %s" % name.__class__.__name__) if not name: raise ValueError("Name must not be empty") - try: - name = u(name) - except UnicodeDecodeError: #pragma NO COVER (on Py3k) - raise ValueError('Name "%s" not decodeable to unicode' % name) - if name in reserved_names: raise ValueError('%s is a reserved name' % name) @@ -578,7 +566,6 @@ def remove(self, name, send_events=True, moving=None, loading=False, attribute of events sent as a result of calling this method will be ``True`` too. """ - name = u(name) other = wrap_if_broken(self.data[name]) oid = get_oid(other, None) diff --git a/substanced/folder/tests/test_init.py b/substanced/folder/tests/test_init.py index 558b5dba..6ded1a31 100644 --- a/substanced/folder/tests/test_init.py +++ b/substanced/folder/tests/test_init.py @@ -11,6 +11,12 @@ verifyClass ) + +BYTES_NAME = b'La Pe\xc3\xb1a' +TEXT_NAME = BYTES_NAME.decode('utf-8') +LATIN1_NAME = TEXT_NAME.encode('latin-1') + + class TestFolder(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -826,8 +832,7 @@ def test_str(self): self.assertTrue(r.endswith('>')) def test_unresolveable_unicode_setitem(self): - from substanced._compat import u - name = u(b'La Pe\xc3\xb1a', 'utf-8').encode('latin-1') + name = LATIN1_NAME folder = self._makeOne() self.assertRaises(ValueError, folder.__setitem__, name, DummyModel()) @@ -839,8 +844,7 @@ def test_resolveable_unicode_setitem(self): self.assertTrue(folder.get(name)) def test_unresolveable_unicode_getitem(self): - from substanced._compat import u - name = u(b'La Pe\xc3\xb1a', 'utf-8').encode('latin-1') + name = LATIN1_NAME folder = self._makeOne() self.assertRaises(UnicodeDecodeError, folder.__getitem__, name) diff --git a/substanced/folder/tests/test_views.py b/substanced/folder/tests/test_views.py index 633f7192..5b4d01db 100644 --- a/substanced/folder/tests/test_views.py +++ b/substanced/folder/tests/test_views.py @@ -7,8 +7,7 @@ from pyramid.httpexceptions import HTTPFound import mock -from substanced._compat import u -_FOOBAR = u('foobar') +_FOOBAR = 'foobar' class Test_name_validator(unittest.TestCase): def _callFUT(self, node, kw): @@ -866,7 +865,9 @@ def test_duplicate_none(self): inst.duplicate() self.assertEqual(context.mock_calls, []) - request.sdiapi.flash_with_undo.assert_called_once_with('Duplicated 0 items', 'success') + request.sdiapi.flash_with_undo.assert_called_once_with( + 'Duplicated 0 items', 'success', + ) request.sdiapi.mgmt_path.assert_called_once_with( context, '@@contents', _query=[], ) @@ -2130,7 +2131,7 @@ def test_upload(self): dummyFileParam = Dummy( type='TYPE', filename='FILENAME', - file=StringIO(u('CONTENT')), + file=StringIO('CONTENT'), ) dummyFileParam.create = mock.Mock( return_value={}, @@ -2172,7 +2173,7 @@ def test_upload_multiple(self): dummyFileParam1 = Dummy( type='TYPE1', filename='FILENAME1', - file=StringIO(u('CONTENT1')), + file=StringIO('CONTENT1'), ) dummyFileParam1.create = mock.Mock( return_value={}, @@ -2180,7 +2181,7 @@ def test_upload_multiple(self): dummyFileParam2 = Dummy( type='TYPE2', filename='FILENAME2', - file=StringIO(u('CONTENT02')), + file=StringIO('CONTENT02'), ) dummyFileParam2.create = mock.Mock( return_value={}, @@ -2188,7 +2189,7 @@ def test_upload_multiple(self): dummyFileParam3 = Dummy( type='TYPE3', filename='FILENAME3', - file=StringIO(u('CONTENT003')), + file=StringIO('CONTENT003'), ) dummyFileParam3.create = mock.Mock( return_value={}, diff --git a/substanced/folder/util.py b/substanced/folder/util.py index 22830fb9..09f4d99e 100644 --- a/substanced/folder/util.py +++ b/substanced/folder/util.py @@ -3,8 +3,6 @@ import os import unidecode -from substanced._compat import u - re_word = re.compile(r'\W+') @@ -12,7 +10,7 @@ def slugify_in_context(context, name, remove_extension=True): if remove_extension: name = os.path.splitext(name)[0] - slug = unidecode.unidecode(u(name)).lower() + slug = unidecode.unidecode(name).lower() orig = slug = re_word.sub('-', slug) i = 1 while True: diff --git a/substanced/folder/views.py b/substanced/folder/views.py index 14aa2770..d45dd281 100644 --- a/substanced/folder/views.py +++ b/substanced/folder/views.py @@ -1,5 +1,5 @@ - import functools +from html import escape import itertools import operator import re @@ -8,9 +8,7 @@ from pyramid.decorator import reify from pyramid.httpexceptions import HTTPFound -from pyramid.security import has_permission -from substanced._compat import escape from substanced.form import FormView from substanced.interfaces import IFolder from substanced.interfaces import IService @@ -22,7 +20,6 @@ find_catalog, get_icon_name, ) -from substanced._compat import u from substanced.file import USE_MAGIC from ..sdi import ( @@ -103,9 +100,9 @@ def rename_duplicated_resource(context, name): m = re.search(r'-(\d+)$', name) if m: new_id = int(m.groups()[0]) + 1 - new_name = name.rsplit('-', 1)[0] + u('-%d') % new_id + new_name = name.rsplit('-', 1)[0] + '-%d' % new_id else: - new_name = name + u('-1') + new_name = name + '-1' if new_name in context: return rename_duplicated_resource(context, new_name) @@ -206,7 +203,7 @@ def get_default_buttons(self): if not 'tomove' in request.session and not 'tocopy' in request.session: can_manage = bool( - has_permission('sdi.manage-contents', context, request) + request.has_permission('sdi.manage-contents', context) ) def delete_enabled_for(folder, resource, request): diff --git a/substanced/form/tests.py b/substanced/form/test_it.py similarity index 97% rename from substanced/form/tests.py rename to substanced/form/test_it.py index ad4503e4..819fbd6a 100644 --- a/substanced/form/tests.py +++ b/substanced/form/test_it.py @@ -176,7 +176,7 @@ def test_setitem_stream_file(self): request = self._makeRequest() inst = self._makeOne(request) here = os.path.dirname(__file__) - thisfile = os.path.join(here, 'tests.py') + thisfile = os.path.join(here, 'test_it.py') with open(thisfile, 'rb') as f: inst['a'] = {'fp': f} randid = inst.session['substanced.tempstore']['a']['randid'] @@ -259,17 +259,15 @@ def _makeOne(self, dirs): def test_functional_using_searchpath(self): from pkg_resources import resource_filename - from .._compat import u default_dir = resource_filename('substanced.form', 'fixtures/') renderer = self._makeOne((default_dir,)) result = renderer('test') - self.assertEqual(result.strip(), u('