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('
Test
')) + self.assertEqual(result.strip(), '
Test
') def test_functional_using_assetspec(self): - from .._compat import u renderer = self._makeOne(()) result = renderer('substanced.form:fixtures/test.pt') - self.assertEqual(result.strip(), u('
Test
')) + self.assertEqual(result.strip(), '
Test
') class DummyWidget(object): pass diff --git a/substanced/locking/__init__.py b/substanced/locking/__init__.py index 2150d481..ae57edba 100644 --- a/substanced/locking/__init__.py +++ b/substanced/locking/__init__.py @@ -17,7 +17,6 @@ import deform.widget from pyramid.location import lineage -from pyramid.security import has_permission from pyramid.threadlocal import get_current_registry from pyramid.traversal import resource_path @@ -46,7 +45,6 @@ get_oid, ) from substanced.schema import Schema -from substanced._compat import INT_TYPES class LockingError(Exception): def __init__(self, lock): @@ -111,7 +109,7 @@ def preparer(self, value): resource = objectmap.object_for(tuple(value.split('/'))) except ValueError: return None - if not has_permission('sdi.lock', resource, request): + if not request.has_permission('sdi.lock', resource): return False return resource @@ -263,7 +261,7 @@ def _get_ownerid(self, owner_or_ownerid): ownerid = get_oid(owner_or_ownerid, None) if ownerid is None: ownerid = owner_or_ownerid - if not isinstance(ownerid, INT_TYPES): + if not isinstance(ownerid, int): raise ValueError( 'Bad value for owner_or_ownerid %r' % owner_or_ownerid ) diff --git a/substanced/objectmap/__init__.py b/substanced/objectmap/__init__.py index ace2dc9a..c857f055 100644 --- a/substanced/objectmap/__init__.py +++ b/substanced/objectmap/__init__.py @@ -4,7 +4,6 @@ import BTrees import colander from persistent import Persistent -from pyramid.compat import is_nonstr_iter from pyramid.security import Allow from pyramid.traversal import ( resource_path_tuple, @@ -22,8 +21,8 @@ set_oid, find_objectmap, wrap_if_broken, + is_nonstr_iter, ) -from .._compat import INT_TYPES """ Pathindex data structure of object map: @@ -39,10 +38,10 @@ >>> map.add('/a/b/c') >>> map.pathindex -{(u'',): {3: set([1])}, - (u'', u'a'): {2: set([1])}, - (u'', u'a', u'b'): {1: set([1])}, - (u'', u'a', u'b', u'c'): {0: set([1])}} +{('',): {3: set([1])}, + ('', 'a'): {2: set([1])}, + ('', 'a', 'b'): {1: set([1])}, + ('', 'a', 'b', 'c'): {0: set([1])}} (Level 0 is "this path") @@ -52,21 +51,21 @@ >>> map.add('/a') >>> map.pathindex -{(u'',): {1: set([2]), 3: set([1])}, - (u'', u'a'): {0: set([2]), 2: set([1])}, - (u'', u'a', u'b'): {1: set([1])}, - (u'', u'a', u'b', u'c'): {0: set([1])}} +{('',): {1: set([2]), 3: set([1])}, + ('', 'a'): {0: set([2]), 2: set([1])}, + ('', 'a', 'b'): {1: set([1])}, + ('', 'a', 'b', 'c'): {0: set([1])}} If we then add '/z' (and its objectid is 3): >>> map.add('/z') >>> map.pathindex -{(u'',): {1: set([2, 3]), 3: set([1])}, - (u'', u'a'): {0: set([2]), 2: set([1])}, - (u'', u'a', u'b'): {1: set([1])}, - (u'', u'a', u'b', u'c'): {0: set([1])}, - (u'', u'z'): {0: set([3])}} +{('',): {1: set([2, 3]), 3: set([1])}, + ('', 'a'): {0: set([2]), 2: set([1])}, + ('', 'a', 'b'): {1: set([1])}, + ('', 'a', 'b', 'c'): {0: set([1])}, + ('', 'z'): {0: set([3])}} And so on and so forth as more items are added. It is an error to attempt to add an item to object map with a path that already exists in the object @@ -79,8 +78,8 @@ >>> map.remove(2) >>> map.pathindex -{(u'',): {1: set([3])}, - (u'', u'z'): {0: set([3])}} +{('',): {1: set([3])}, + ('', 'z'): {0: set([3])}} """ @@ -131,7 +130,7 @@ def _get_path_tuple(self, obj_objectid_or_path_tuple): path_tuple = None if hasattr(obj_objectid_or_path_tuple, '__parent__'): path_tuple = resource_path_tuple(obj_objectid_or_path_tuple) - elif isinstance(obj_objectid_or_path_tuple, INT_TYPES): + elif isinstance(obj_objectid_or_path_tuple, int): path_tuple = self.objectid_to_path.get(obj_objectid_or_path_tuple) elif isinstance(obj_objectid_or_path_tuple, tuple): path_tuple = obj_objectid_or_path_tuple diff --git a/substanced/objectmap/evolve.py b/substanced/objectmap/evolve.py index bef85d88..3039c520 100644 --- a/substanced/objectmap/evolve.py +++ b/substanced/objectmap/evolve.py @@ -2,13 +2,12 @@ import BTrees -from .._compat import u from ..util import ( get_acl, postorder, ) -_SLASH = u('/') +_SLASH = '/' logger = getLogger(__name__) diff --git a/substanced/objectmap/tests/test_init.py b/substanced/objectmap/tests/test_init.py index b5c642b0..916e93da 100644 --- a/substanced/objectmap/tests/test_init.py +++ b/substanced/objectmap/tests/test_init.py @@ -6,13 +6,12 @@ IS_32_BIT = sys.maxsize == 2**32 -from substanced._compat import u -_BLANK = u('') -_SLASH = u('/') -_A = u('a') -_B = u('b') -_C = u('c') -_Z = u('z') +_BLANK = '' +_SLASH = '/' +_A = 'a' +_B = 'b' +_C = 'c' +_Z = 'z' class TestObjectMap(unittest.TestCase): def setUp(self): diff --git a/substanced/principal/__init__.py b/substanced/principal/__init__.py index 7fa3e109..20cd819c 100644 --- a/substanced/principal/__init__.py +++ b/substanced/principal/__init__.py @@ -2,8 +2,8 @@ import os import string +import bcrypt from persistent import Persistent -from cryptacular.bcrypt import BCRYPTPasswordManager from zope.interface import ( implementer, @@ -62,11 +62,10 @@ acquire, ) from ..stats import statsd_gauge -from .._compat import _LETTERS def _gen_random_token(): length = random.choice(range(10, 16)) - chars = _LETTERS + string.digits + chars = string.ascii_letters + string.digits return ''.join(random.choice(chars) for _ in range(length)) @service( @@ -376,16 +375,21 @@ class User(Folder): """ Represents a user. """ tzname = 'UTC' # backwards compatibility default - pwd_manager = BCRYPTPasswordManager() - groupids = multireference_sourceid_property(UserToGroup) groups = multireference_source_property(UserToGroup) name = renamer() + @staticmethod + def hash_new_password(password): + if isinstance(password, str): + password = password.encode('utf-8') + + return bcrypt.hashpw(password, bcrypt.gensalt()) + def __init__(self, password=None, email=None, tzname=None, locale=None): Folder.__init__(self) if password is not None: - password = self.pwd_manager.encode(password) + password = self.hash_new_password(password) self.password = password self.email = email if tzname is None: @@ -411,10 +415,12 @@ def check_password(self, password): # avoid DOS ala # https://www.djangoproject.com/weblog/2013/sep/15/security/ raise ValueError('Not checking password > 4096 bytes') - return self.pwd_manager.check(self.password, password) + if isinstance(password, str): + password = password.encode('utf-8') + return bcrypt.checkpw(password, self.password) def set_password(self, password): - self.password = self.pwd_manager.encode(password) + self.password = self.hash_new_password(password) def email_password_reset(self, request): """ Sends a password reset email.""" diff --git a/substanced/principal/subscribers.py b/substanced/principal/subscribers.py index fe24ddb9..139c5959 100644 --- a/substanced/principal/subscribers.py +++ b/substanced/principal/subscribers.py @@ -19,7 +19,6 @@ set_acl, find_service, ) -from .._compat import INT_TYPES @subscribe_added(IUser) def user_added(event): @@ -100,12 +99,11 @@ def principal_added(event): 'user with the login name %s' % principal_name ) -_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/principal/tests/test_principal.py b/substanced/principal/tests/test_principal.py index a7c2c413..13cfe06b 100644 --- a/substanced/principal/tests/test_principal.py +++ b/substanced/principal/tests/test_principal.py @@ -1,6 +1,8 @@ import unittest -from pyramid import testing + +import bcrypt import colander +from pyramid import testing from zope.interface import implementer class Test_locale_widget(unittest.TestCase): @@ -356,7 +358,7 @@ def _makeOne(self, password, email='', tzname=None): def test___dump__(self): inst = self._makeOne('abc') result = inst.__dump__() - self.assertTrue(inst.pwd_manager.check(result['password'], 'abc')) + self.assertTrue(bcrypt.checkpw(b'abc', result['password'])) def test_check_password(self): inst = self._makeOne('abc') @@ -370,7 +372,7 @@ def test_check_password_gt_4096_bytes(self): def test_set_password(self): inst = self._makeOne('abc') inst.set_password('abcdef') - self.assertTrue(inst.pwd_manager.check(inst.password, 'abcdef')) + self.assertTrue(bcrypt.checkpw(b'abcdef', inst.password)) def test_email_password_reset(self): from ...testing import make_site diff --git a/substanced/property/__init__.py b/substanced/property/__init__.py index 2043cc0c..be70e7e9 100644 --- a/substanced/property/__init__.py +++ b/substanced/property/__init__.py @@ -5,13 +5,13 @@ Interface, ) -from pyramid.compat import is_nonstr_iter from walkabout import IPredicateDomain from walkabout import PredicateDomain from ..interfaces import IPropertySheet from ..event import ObjectModified from ..content import _ContentTypePredicate +from ..util import is_nonstr_iter try: # pyramid 1.9 and below diff --git a/substanced/property/views.py b/substanced/property/views.py index 159d2152..b3ec8d6c 100644 --- a/substanced/property/views.py +++ b/substanced/property/views.py @@ -3,10 +3,6 @@ HTTPForbidden, HTTPNotFound, ) -from pyramid.security import ( - authenticated_userid, - has_permission, - ) from ..form import FormError from ..form import FormView @@ -29,7 +25,7 @@ def has_permission_to_view_any_propertysheet(context, request): return True view_permission = dict(permissions).get('view') if view_permission: - if has_permission(view_permission, context, request): + if request.has_permission(view_permission, context): return True else: return True @@ -71,7 +67,7 @@ def has_permission_to(self, perm, sheet_factory): if permissions is not None: permission = dict(permissions).get(perm) if permission: - return has_permission(permission, self.context, self.request) + return self.request.has_permission(permission, self.context) return True def viewable_sheet_factories(self): @@ -93,7 +89,7 @@ def save_success(self, appstruct): "You don't have permission to change properties of this " "property sheet") try: - ownerid = authenticated_userid(self.request) + ownerid = self.request.authenticated_userid if could_lock_resource(self.context, ownerid): #may raise changed = self.active_sheet.set(appstruct) except LockError as e: diff --git a/substanced/root/tests.py b/substanced/root/test_it.py similarity index 100% rename from substanced/root/tests.py rename to substanced/root/test_it.py diff --git a/substanced/scaffolds/tests.py b/substanced/scaffolds/test_it.py similarity index 100% rename from substanced/scaffolds/tests.py rename to substanced/scaffolds/test_it.py diff --git a/substanced/schema/tests.py b/substanced/schema/test_it.py similarity index 100% rename from substanced/schema/tests.py rename to substanced/schema/test_it.py diff --git a/substanced/sdi/__init__.py b/substanced/sdi/__init__.py index bee08bbe..cc307ca5 100644 --- a/substanced/sdi/__init__.py +++ b/substanced/sdi/__init__.py @@ -27,11 +27,7 @@ get_renderer, ) from pyramid.request import Request -from pyramid.security import ( - authenticated_userid, - has_permission, - ) -from pyramid.session import UnencryptedCookieSessionFactoryConfig +from pyramid.session import SignedCookieSessionFactory from pyramid.util import ( TopologicalSorter, @@ -411,7 +407,7 @@ def default_sdi_addable(context, intr): def user(request): context = request.context - userid = authenticated_userid(request) + userid = request.authenticated_userid if userid is None: return None adapter = request.registry.queryAdapter((context, request), IUserLocator) @@ -452,7 +448,7 @@ def get_flash_with_undo_snippet(self, msg, queue='', allow_duplicate=True): conn = self.get_connection(request) db = conn.db() snippet = msg - has_perm = has_permission('sdi.undo', request.context, request) + has_perm = request.has_permission('sdi.undo', request.context) if db.supportsUndo() and has_perm: hsh = str(id(request)) + str(hash(msg)) t = self.transaction.get() @@ -506,7 +502,7 @@ def breadcrumbs(self): request = self.request breadcrumbs = [] for resource in lineage(request.context): - if not has_permission('sdi.view', resource, request): + if not request.has_permission('sdi.view', resource): return [] url = request.sdiapi.mgmt_path(resource, '@@manage_main') name = getattr(resource, 'sdi_title', None) @@ -568,7 +564,7 @@ def includeme(config): # pragma: no cover if secret is None: raise ConfigurationError( 'You must set a substanced.secret key in your .ini file') - session_factory = UnencryptedCookieSessionFactoryConfig(secret) + session_factory = SignedCookieSessionFactory(secret) config.set_session_factory(session_factory) from ..principal import groupfinder # NB: we use the AuthTktAuthenticationPolicy rather than the session diff --git a/substanced/sdi/tests/test_sdi.py b/substanced/sdi/tests/test_sdi.py index 9c3f3a49..2064c320 100644 --- a/substanced/sdi/tests/test_sdi.py +++ b/substanced/sdi/tests/test_sdi.py @@ -892,7 +892,6 @@ def test_flash_with_undo_db_doesnt_support_undo(self): self.assertFalse(inst.transaction.notes) def test_flash_with_undo_gardenpath(self): - from ..._compat import u self.config.testing_securitypolicy(permissive=True) request = testing.DummyRequest() inst = self._makeOne(request) @@ -902,8 +901,8 @@ def test_flash_with_undo_gardenpath(self): inst.mgmt_path = lambda *arg, **kw: '/mg' inst.flash_with_undo('message') self.assertEqual(request.session['_f_info'], - [u('message Undo\n')]) + ['message Undo\n']) self.assertTrue(inst.transaction.notes) def test_flash_gardenpath(self): diff --git a/substanced/sdi/views/acl.py b/substanced/sdi/views/acl.py index c5331e6c..daee6340 100644 --- a/substanced/sdi/views/acl.py +++ b/substanced/sdi/views/acl.py @@ -8,8 +8,7 @@ Everyone, Authenticated, ) -from pyramid.compat import is_nonstr_iter -from pyramid.session import check_csrf_token +from pyramid.csrf import check_csrf_token from pyramid.view import view_defaults from pyramid.location import lineage @@ -20,8 +19,8 @@ get_all_permissions, set_acl, find_service, + is_nonstr_iter, ) -from ..._compat import STRING_TYPES from ...util import _ from .. import mgmt_view @@ -203,7 +202,7 @@ def get_local_acl(self): break if permissions == ALL_PERMISSIONS: permissions = ('-- ALL --',) - if (isinstance(permissions, STRING_TYPES) or + if (isinstance(permissions, str) or not hasattr(permissions, '__iter__')): permissions = (permissions,) pname = self.get_principal_name(principal_id) diff --git a/substanced/sdi/views/folder.py b/substanced/sdi/views/folder.py index 8252dc23..5c5bad92 100644 --- a/substanced/sdi/views/folder.py +++ b/substanced/sdi/views/folder.py @@ -1,2 +1,2 @@ # b/c shim for old location -from substanced.folder.views import * +from substanced.folder.views import * # pragma: NO COVER diff --git a/substanced/sdi/views/login.py b/substanced/sdi/views/login.py index 25b4306a..edfd010c 100644 --- a/substanced/sdi/views/login.py +++ b/substanced/sdi/views/login.py @@ -3,7 +3,7 @@ HTTPFound ) from pyramid.renderers import get_renderer -from pyramid.session import check_csrf_token +from pyramid.csrf import check_csrf_token from pyramid.security import ( remember, forget, diff --git a/substanced/sdi/views/tests/test_acl.py b/substanced/sdi/views/tests/test_acl.py index 399c3f34..f8837300 100644 --- a/substanced/sdi/views/tests/test_acl.py +++ b/substanced/sdi/views/tests/test_acl.py @@ -2,9 +2,8 @@ from pyramid import testing -from ...._compat import u -_JOHN = u('john') -_MARY = u('mary') +_JOHN = 'john' +_MARY = 'mary' class TestACLView(unittest.TestCase): def setUp(self): diff --git a/substanced/sdi/views/tests/test_undo.py b/substanced/sdi/views/tests/test_undo.py index ccfd88ed..f390f9c5 100644 --- a/substanced/sdi/views/tests/test_undo.py +++ b/substanced/sdi/views/tests/test_undo.py @@ -175,7 +175,9 @@ def find_objectmap(ctx): def test_undo_multiple(self): import binascii + sec_pol = testing.DummySecurityPolicy(userid="phred") request = testing.DummyRequest() + request._get_authentication_policy = lambda: sec_pol context = testing.DummyResource() inst = self._makeOne(context, request) conn = DummyConnection() @@ -183,9 +185,6 @@ def get_connection(req): self.assertEqual(req, request) return conn inst.get_connection = get_connection - def authenticated_userid(req): - self.assertEqual(req, request) - return 1 post = testing.DummyResource() enca = binascii.b2a_base64(b'a') encb = binascii.b2a_base64(b'b') @@ -196,18 +195,19 @@ def getall(n): post.getall = getall request.POST = post request.sdiapi = DummySDIAPI() - inst.authenticated_userid = authenticated_userid txn = DummyTransaction() inst.transaction = txn result = inst.undo_multiple() self.assertEqual(result.location, '/mgmt_path') self.assertEqual(conn._db.tids, [b'a', b'b']) self.assertTrue(txn.committed) - self.assertEqual(txn.user, 1) + self.assertEqual(txn.user, "phred") def test_undo_multiple_with_text_in_POST(self): import binascii + sec_pol = testing.DummySecurityPolicy(userid="phred") request = testing.DummyRequest() + request._get_authentication_policy = lambda: sec_pol context = testing.DummyResource() inst = self._makeOne(context, request) conn = DummyConnection() @@ -215,9 +215,6 @@ def get_connection(req): self.assertEqual(req, request) return conn inst.get_connection = get_connection - def authenticated_userid(req): - self.assertEqual(req, request) - return 1 post = testing.DummyResource() enca = binascii.b2a_base64(b'a').decode('ascii') encb = binascii.b2a_base64(b'b').decode('ascii') @@ -228,14 +225,13 @@ def getall(n): post.getall = getall request.POST = post request.sdiapi = DummySDIAPI() - inst.authenticated_userid = authenticated_userid txn = DummyTransaction() inst.transaction = txn result = inst.undo_multiple() self.assertEqual(result.location, '/mgmt_path') self.assertEqual(conn._db.tids, [b'a', b'b']) self.assertTrue(txn.committed) - self.assertEqual(txn.user, 1) + self.assertEqual(txn.user, "phred") def test_undo_multiple_with_exception(self): import binascii @@ -249,9 +245,6 @@ def get_connection(req): return conn conn._db.undo_exc = POSError inst.get_connection = get_connection - def authenticated_userid(req): - self.assertEqual(req, request) - return 1 post = testing.DummyResource() enca = binascii.b2a_base64(b'a') encb = binascii.b2a_base64(b'b') @@ -262,7 +255,6 @@ def getall(n): post.getall = getall request.POST = post request.sdiapi = DummySDIAPI() - inst.authenticated_userid = authenticated_userid txn = DummyTransaction() inst.transaction = txn result = inst.undo_multiple() diff --git a/substanced/sdi/views/undo.py b/substanced/sdi/views/undo.py index 5d241d01..b836c335 100644 --- a/substanced/sdi/views/undo.py +++ b/substanced/sdi/views/undo.py @@ -4,7 +4,6 @@ from pyramid_zodbconn import get_connection from pyramid.httpexceptions import HTTPFound -from pyramid.security import authenticated_userid from .. import mgmt_view from ...objectmap import find_objectmap @@ -14,7 +13,6 @@ class UndoViews(object): transaction = transaction # for tests get_connection = staticmethod(get_connection) # for tests find_objectmap = staticmethod(find_objectmap) # for tests - authenticated_userid = staticmethod(authenticated_userid) # for tests def __init__(self, context, request): self.context = context @@ -37,7 +35,7 @@ def undo_recent(self): undohash = request.params['undohash'] undo = None db = self._get_db() - userid = self.authenticated_userid(request) + userid = self.request.authenticated_userid # I am permitted to undo it if: # # - It happend within the most recent 50 transactions. @@ -88,7 +86,7 @@ def undo_multiple(self): tids = [] descriptions = [] - uid = self.authenticated_userid(request) + uid = request.authenticated_userid for tid in transaction_info: if not isinstance(tid, bytes): #pragma NO COVER Py3k diff --git a/substanced/util/__init__.py b/substanced/util/__init__.py index b07f800c..f10db34f 100644 --- a/substanced/util/__init__.py +++ b/substanced/util/__init__.py @@ -1,4 +1,8 @@ import calendar +try: + import cProfile as _profile +except ImportError: # pragma: no cover (pypy) + import profile as _profile import itertools import json import math @@ -6,31 +10,21 @@ import pstats import tempfile import types -from ZODB.interfaces import IBroken -try: - import cProfile as _profile -except ImportError: # pragma: no cover (pypy) - import profile as _profile - -from zope.interface import providedBy -from zope.interface.declarations import Declaration +from urllib.parse import parse_qsl +from urllib.parse import urlsplit +from urllib.parse import urlunsplit from pyramid.location import lineage from pyramid.threadlocal import get_current_registry from pyramid.i18n import TranslationStringFactory from pyramid.encode import urlencode +from ZODB.interfaces import IBroken +from zope.interface import providedBy +from zope.interface.declarations import Declaration from ..interfaces import IFolder from ..interfaces import IService -from .._compat import ( - parse_qsl, - urlsplit, - urlunsplit, - STRING_TYPES, - INT_TYPES, - ) - _ = TranslationStringFactory('substanced') @@ -581,8 +575,7 @@ def get_principal_repr(principal_or_id): Given any other string value, return it. """ - base_types = STRING_TYPES + INT_TYPES - if isinstance(principal_or_id, base_types): + if isinstance(principal_or_id, (str, int)): return str(principal_or_id) prepr = getattr(principal_or_id, '__principal_repr__', None) if prepr is not None: @@ -662,3 +655,5 @@ def profile( finally: os.remove(fn) +def is_nonstr_iter(v): + return (not isinstance(v, str)) and hasattr(v, '__iter__') diff --git a/substanced/util/tests.py b/substanced/util/test_it.py similarity index 99% rename from substanced/util/tests.py rename to substanced/util/test_it.py index 20a78613..a67cadf3 100644 --- a/substanced/util/tests.py +++ b/substanced/util/test_it.py @@ -1,7 +1,6 @@ import unittest from pyramid import testing -from pyramid.compat import text_ from . import _marker @@ -358,7 +357,9 @@ def test_with_quoted_strings_in_url(self): def test_with_nonascii_values_in_kw(self): url = 'http://example.com?c=%2B' - result = self._callFUT(url, a=text_(b'LaPe\xc3\xb1a', 'utf-8')) + result = self._callFUT( + url, a=b'LaPe\xc3\xb1a'.decode('utf-8', errors='strict') + ) self.assertEqual(result, 'http://example.com?a=LaPe%C3%B1a&c=%2B') class Test_acquire(unittest.TestCase): @@ -519,7 +520,9 @@ def test_module(self): def test_nonmodule(self): result = self._callFUT(self.__class__) - self.assertEqual(result, 'substanced.util.tests.Test_get_dotted_name') + self.assertEqual( + result, 'substanced.util.test_it.Test_get_dotted_name', + ) class Test_get_content_type(unittest.TestCase): def setUp(self): @@ -717,7 +720,7 @@ def test_has_ft_attr(self): def test_without_ft_attr(self): resource = Dummy() self.assertEqual(self._callFUT(resource), - 'substanced.util.tests.Dummy') + 'substanced.util.test_it.Dummy') class Test_get_interfaces(unittest.TestCase): def _callFUT(self, resource, classes=True): diff --git a/substanced/workflow/__init__.py b/substanced/workflow/__init__.py index 40e603fe..7bf4c82b 100644 --- a/substanced/workflow/__init__.py +++ b/substanced/workflow/__init__.py @@ -8,7 +8,6 @@ from pyramid.security import Authenticated from pyramid.security import DENY_ALL from pyramid.security import Everyone -from pyramid.security import has_permission from zope.interface import implementer from ..event import AfterTransition @@ -71,7 +70,7 @@ def add_state(self, state_name, callback=None, **kw): :meth:`Workflow.transition_to_state` will trigger callback if entering this state. :type callback: callable - :param \*\*kw: Metadata assigned to this state. + :param kw: Metadata assigned to this state. :raises: :exc:`WorkflowError` if state already exists. @@ -99,7 +98,7 @@ def add_transition(self, transition_name, from_state, to_state, :meth:`Workflow.transition_to_state` will trigger callback if this transition is executed. :type callback: callable - :param \*\*kw: Metadata assigned to this transition. + :param kw: Metadata assigned to this transition. :raises: :exc:`WorkflowError` if transition already exists. :raises: :exc:`WorkflowError` if from_state or to_state don't exist. @@ -221,8 +220,7 @@ def get_states(self, content, request, from_state=None): for transition in state['transitions']: permission = transition.get('permission') if permission is not None: - if not has_permission(permission, content, - request): + if not request.has_permission(permission, content): continue L.append(transition) state['transitions'] = L @@ -289,7 +287,7 @@ def _transition(self, content, transition_name, context=None, permission = transition.get('permission') if permission is not None: - if not has_permission(permission, context, request): + if not request.has_permission(permission, context): raise WorkflowError( '%s permission required for transition using %r' % ( permission, self.name) @@ -401,8 +399,7 @@ def get_transitions(self, content, request, from_state=None): for transition in transitions: permission = transition.get('permission') if permission is not None: - if not has_permission(permission, content, - request): + if not request.has_permission(permission, content): continue L.append(transition) return L diff --git a/substanced/workflow/tests/test_workflow.py b/substanced/workflow/tests/test_workflow.py index 3bf50dec..867c0fd1 100644 --- a/substanced/workflow/tests/test_workflow.py +++ b/substanced/workflow/tests/test_workflow.py @@ -49,9 +49,7 @@ def _makePopulatedOverlappingTransitions( ) return sm - @mock.patch('substanced.workflow.has_permission') - def test_transition_to_state_two_transitions_second_works( - self, mock_has_permission): + def test_transition_to_state_two_transitions_second_works(self): args = [] def dummy(content, **info): args.append((content, info)) @@ -63,17 +61,19 @@ def dummy(content, **info): sm._transitions['submit']['permission'] = 'forbidden' sm._transitions['submit2']['permission'] = 'allowed' - mock_has_permission.side_effect = lambda p, c, r: p != 'forbidden' ob = DummyContent() request = testing.DummyRequest() + request.has_permission = mock.Mock( + spec_set=(), side_effect = lambda p, c: p != 'forbidden', + ) ob.__workflow_state__ = {'basic': 'private'} sm.transition_to_state(ob, request, 'pending') self.assertEqual(len(args), 1) self.assertEqual(args[0][1]['transition']['name'], 'submit2') - @mock.patch('substanced.workflow.has_permission') - def test_transition_to_state_two_transitions_none_works( - self, mock_has_permission): + def test_transition_to_state_two_transitions_none_works(self): + from substanced.workflow import WorkflowError + callback_args = [] def dummy(content, info): # pragma NO COVER callback_args.append((content, info)) @@ -88,14 +88,15 @@ def dummy(content, info): # pragma NO COVER ob = DummyContent() ob.__workflow_state__ = {'basic': 'private'} request = testing.DummyRequest() - from substanced.workflow import WorkflowError - mock_has_permission.return_value = False + hp = request.has_permission = mock.Mock( + spec_set=(), return_value=False, + ) self.assertRaises(WorkflowError, sm.transition_to_state, ob, request, 'pending') self.assertEqual(len(callback_args), 0) - pcalls = sorted(mock_has_permission.mock_calls) - self.assertEqual(pcalls[0], mock.call('forbidden1', ob, request)) - self.assertEqual(pcalls[1], mock.call('forbidden2', ob, request)) + pcalls = sorted(hp.mock_calls) + self.assertEqual(pcalls[0], mock.call('forbidden1', ob)) + self.assertEqual(pcalls[1], mock.call('forbidden2', ob)) def test_class_conforms_to_IWorkflow(self): from zope.interface.verify import verifyClass @@ -646,9 +647,7 @@ def append(content, name, context=None, request=None): self.assertEqual(transitioned['request'], request) self.assertEqual(transitioned['context'], None) - @mock.patch('substanced.workflow.has_permission') - def test_transition_to_state_not_permissive(self, mock_has_permission): - mock_has_permission.return_value = False + def test_transition_to_state_not_permissive(self): workflow = self._makeOne() transitioned = [] def append(content, name, context=None, request=None, @@ -657,7 +656,10 @@ def append(content, name, context=None, request=None, 'context': context, 'skip_same': skip_same} transitioned.append(D) workflow._transition_to_state = lambda *arg, **kw: append(*arg, **kw) - request = object() + request = testing.DummyRequest() + request.has_permission = mock.Mock( + spec_set=(), return_value = False, + ) content = DummyContent() content.__workflow_state__ = {'basic': 'pending'} workflow.transition_to_state(content, request, 'published') @@ -710,58 +712,66 @@ def append(content, name, context=None, request=None, self.assertEqual(transitioned['context'], None) self.assertEqual(transitioned['skip_same'], True) - @mock.patch('substanced.workflow.has_permission') - def test_get_transitions_permissive(self, mock_has_permission): - mock_has_permission.return_value = True + def test_get_transitions_permissive(self): + request = testing.DummyRequest() + hp = request.has_permission = mock.Mock( + spec_set=(), return_value=True, + ) + content = object() workflow = self._makeOne() workflow._get_transitions = \ lambda *arg, **kw: [{'permission': 'view'}, {}] - transitions = workflow.get_transitions(None, None, 'private') + transitions = workflow.get_transitions(content, request, 'private') self.assertEqual(len(transitions), 2) - self.assertEqual(mock_has_permission.mock_calls, - [mock.call('view', None, None)]) + self.assertEqual(hp.mock_calls, [mock.call('view', content)]) - @mock.patch('substanced.workflow.has_permission') - def test_get_transitions_nonpermissive(self, mock_has_permission): - mock_has_permission.return_value = False + def test_get_transitions_nonpermissive(self): + request = testing.DummyRequest() + hp = request.has_permission = mock.Mock( + spec_set=(), return_value=False, + ) + content = object() workflow = self._makeOne() workflow._get_transitions = \ lambda *arg, **kw: [{'permission': 'view'}, {}] - transitions = workflow.get_transitions(None, 'private') + transitions = workflow.get_transitions(content, request, 'private') self.assertEqual(len(transitions), 1) - self.assertEqual(mock_has_permission.mock_calls, - [mock.call('view', None, 'private')]) + self.assertEqual(hp.mock_calls, [mock.call('view', content)]) - @mock.patch('substanced.workflow.has_permission') - def test_get_states_permissive(self, mock_has_permission): - mock_has_permission.return_value = True + def test_get_states_permissive(self): + request = testing.DummyRequest() + hp = request.has_permission = mock.Mock( + spec_set=(), return_value=True, + ) state_info = [] state_info.append({'transitions': [{'permission': 'view'}, {}]}) state_info.append({'transitions': [{'permission': 'view'}, {}]}) + content = object() workflow = self._makeOne() workflow._get_states = lambda *arg, **kw: state_info - request = object() - result = workflow.get_states(request, 'whatever') + result = workflow.get_states(content, request, 'whatever') self.assertEqual(result, state_info) - self.assertEqual(mock_has_permission.mock_calls, - [mock.call('view', request, 'whatever'), - mock.call('view', request, 'whatever')]) + self.assertEqual(hp.mock_calls, + [mock.call('view', content), + mock.call('view', content)]) - @mock.patch('substanced.workflow.has_permission') - def test_get_states_nonpermissive(self, mock_has_permission): - mock_has_permission.return_value = False + def test_get_states_nonpermissive(self): + request = testing.DummyRequest() + hp = request.has_permission = mock.Mock( + spec_set=(), return_value=False, + ) state_info = [] state_info.append({'transitions': [{'permission': 'view'}, {}]}) state_info.append({'transitions': [{'permission': 'view'}, {}]}) + content = object() workflow = self._makeOne() workflow._get_states = lambda *arg, **kw: state_info - request = testing.DummyRequest() - result = workflow.get_states(request, 'whatever') + result = workflow.get_states(content, request, 'whatever') self.assertEqual(result, [{'transitions': [{}]}, {'transitions': [{}]}]) - self.assertEqual(mock_has_permission.mock_calls, - [mock.call('view', request, 'whatever'), - mock.call('view', request, 'whatever')]) + self.assertEqual(hp.mock_calls, + [mock.call('view', content), + mock.call('view', content)]) def test_callbackinfo_has_request(self): def transition_cb(content, **info): @@ -967,40 +977,37 @@ def _callFUT(self, config, workflow, type_, def test_register_workflow_global(self): from substanced.interfaces import IDefaultWorkflow - from substanced._compat import u wf = mock.Mock() self._callFUT(self.config, wf, 'basic') self.assertEqual({'basic': {IDefaultWorkflow: wf}}, self.config.registry.workflow.types) - self.assertEqual({IDefaultWorkflow: {u('basic'): wf}}, + self.assertEqual({IDefaultWorkflow: {'basic': wf}}, self.config.registry.workflow.content_types) def test_register_workflow_global_skip_if_exists(self): from substanced.interfaces import IDefaultWorkflow - from substanced._compat import u wf = mock.Mock() self._callFUT(self.config, wf, 'basic') self.assertEqual({'basic': {IDefaultWorkflow: wf}}, self.config.registry.workflow.types) - self.assertEqual({IDefaultWorkflow: {u('basic'): wf}}, + self.assertEqual({IDefaultWorkflow: {'basic': wf}}, self.config.registry.workflow.content_types) self._callFUT(self.config, wf, 'basic') self.assertEqual({'basic': {IDefaultWorkflow: wf}}, self.config.registry.workflow.types) - self.assertEqual({IDefaultWorkflow: {u('basic'): wf}}, + self.assertEqual({IDefaultWorkflow: {'basic': wf}}, self.config.registry.workflow.content_types) def test_register_workflow_two_types(self): - from substanced._compat import u wf = mock.Mock() self._callFUT(self.config, wf, 'basic', 'File') self._callFUT(self.config, wf, 'basic', 'Folder') self.assertEqual({'basic': {'File': wf, 'Folder': wf}}, self.config.registry.workflow.types) - self.assertEqual({'File': {u('basic'): wf}, 'Folder': {u('basic'): wf}}, + self.assertEqual({'File': {'basic': wf}, 'Folder': {'basic': wf}}, self.config.registry.workflow.content_types) self.config.registry.content.exists.assert_any_call('File') self.config.registry.content.exists.assert_any_call('Folder') diff --git a/test_requirements.txt b/test_requirements.txt index 71dab096..45088b32 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,3 +1,3 @@ coverage -nose +py.test tox diff --git a/tox.ini b/tox.ini index 24b60797..d57a0649 100644 --- a/tox.ini +++ b/tox.ini @@ -1,32 +1,32 @@ [tox] envlist = - py27, - py35, - py36, - py37, py38, + py39, + py310, + py311, + py312, cover, docs, [testenv] commands = - {envbindir}/nosetests -q {posargs} + {envbindir}/py.test -q {posargs} deps = - nose - coverage - mock + pytest + pytest-cov [testenv:cover] basepython = - python2.7 + python3.12 commands = - {envbindir}/nosetests --with-xunit --with-xcoverage + {envbindir}/py.test -q --cov=substanced --cov-fail-under=100 deps = - nosexcover + pytest + pytest-cov [testenv:docs] basepython = - python2.7 + python3.12 commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html deps =