From 938117afb5b15e268ee8811fdaec929671d1346b Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Mon, 14 Jul 2014 14:26:47 -0400 Subject: [PATCH 01/10] [#1841] add IValidators, IConverters plugin interfaces --- ckan/logic/__init__.py | 18 ++++++++++++++++++ ckan/plugins/interfaces.py | 23 +++++++++++++++++++++++ ckan/plugins/toolkit.py | 4 ++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py index 49fbc0d7beb..2e2da720a10 100644 --- a/ckan/logic/__init__.py +++ b/ckan/logic/__init__.py @@ -624,6 +624,15 @@ def get_validator(validator): validators = _import_module_functions('ckan.logic.validators') _validators_cache.update(validators) _validators_cache.update({'OneOf': formencode.validators.OneOf}) + + for plugin in p.PluginImplementations(p.IValidators): + for name, validator in plugin.get_validators().items(): + if name in _validators_cache: + raise Exception( + 'The validator %r is already defined' % (name,) + ) + log.debug('Validator function {0} from plugin {1} was inserted'.format(name, plugin.name)) + _validators_cache[name] = validator try: return _validators_cache[validator] except KeyError: @@ -662,6 +671,15 @@ def get_converter(converter): if not _converters_cache: converters = _import_module_functions('ckan.logic.converters') _converters_cache.update(converters) + + for plugin in p.PluginImplementations(p.IConverters): + for name, converter in plugin.get_converters().items(): + if name in _converters_cache: + raise Exception( + 'The converter %r is already defined' % (name,) + ) + log.debug('Converter function {0} from plugin {1} was inserted'.format(name, plugin.name)) + _converters_cache[name] = validator try: return _converters_cache[converter] except KeyError: diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index 50380bdf471..607bc41ebbb 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -13,6 +13,7 @@ 'IPackageController', 'IPluginObserver', 'IConfigurable', 'IConfigurer', 'IActions', 'IResourceUrlChange', 'IDatasetForm', + 'IValidators', 'IConverters', 'IResourcePreview', 'IResourceController', 'IGroupForm', @@ -523,6 +524,28 @@ def get_actions(self): """ +class IValidators(Interface): + """ + Allow adding of validators to be returned by ``get_validator()``. + """ + def get_validators(self): + """ + Should return a dict, the keys being the name of the validator + function and the values being the functions themselves. + """ + + +class IConverters(Interface): + """ + Allow adding of converters to be returned by ``get_converter()``. + """ + def get_converters(self): + """ + Should return a dict, the keys being the name of the converter + function and the values being the functions themselves. + """ + + class IAuthFunctions(Interface): '''Override CKAN's authorization functions, or add new auth functions.''' diff --git a/ckan/plugins/toolkit.py b/ckan/plugins/toolkit.py index 5c45b21a70b..d8ad2723e64 100644 --- a/ckan/plugins/toolkit.py +++ b/ckan/plugins/toolkit.py @@ -44,8 +44,8 @@ class _Toolkit(object): 'aslist', # converts an object to a list 'literal', # stop tags in a string being escaped 'get_action', # get logic action function - 'get_converter', # get validator function - 'get_validator', # get convertor action function + 'get_converter', # get navl schema converter + 'get_validator', # get navl schema validator 'check_access', # check logic function authorisation 'navl_validate', # implements validate method with navl schema 'ObjectNotFound', # action not found exception From 683c7fff99c5ab40cc688fa8a570418e9e11843c Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Thu, 7 Aug 2014 14:03:28 -0400 Subject: [PATCH 02/10] [#1841] define NameConflict error instead of using Excepion itself --- ckan/logic/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py index 2e2da720a10..f9971be6166 100644 --- a/ckan/logic/__init__.py +++ b/ckan/logic/__init__.py @@ -16,6 +16,10 @@ _validate = df.validate +class NameConflict(Exception): + pass + + class AttributeDict(dict): def __getattr__(self, name): try: @@ -377,7 +381,7 @@ def get_action(action): for plugin in p.PluginImplementations(p.IActions): for name, auth_function in plugin.get_actions().items(): if name in resolved_action_plugins: - raise Exception( + raise NameConflict( 'The action %r is already implemented in %r' % ( name, resolved_action_plugins[name] @@ -628,7 +632,7 @@ def get_validator(validator): for plugin in p.PluginImplementations(p.IValidators): for name, validator in plugin.get_validators().items(): if name in _validators_cache: - raise Exception( + raise NameConflict( 'The validator %r is already defined' % (name,) ) log.debug('Validator function {0} from plugin {1} was inserted'.format(name, plugin.name)) @@ -675,7 +679,7 @@ def get_converter(converter): for plugin in p.PluginImplementations(p.IConverters): for name, converter in plugin.get_converters().items(): if name in _converters_cache: - raise Exception( + raise NameConflict( 'The converter %r is already defined' % (name,) ) log.debug('Converter function {0} from plugin {1} was inserted'.format(name, plugin.name)) From 3770777352c7a7dabf35f0b2d8a0eaaa517379f3 Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Thu, 7 Aug 2014 16:06:07 -0400 Subject: [PATCH 03/10] [#1841] extras_unicode_convert is not a validator --- ckan/logic/converters.py | 5 +++++ ckan/logic/schema.py | 2 +- ckan/logic/validators.py | 5 ----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ckan/logic/converters.py b/ckan/logic/converters.py index 17e355dac10..b2a7f45209e 100644 --- a/ckan/logic/converters.py +++ b/ckan/logic/converters.py @@ -34,6 +34,11 @@ def remove_from_extras(data, key): return remove_from_extras(data, data_key[1]) +def extras_unicode_convert(extras, context): + for extra in extras: + extras[extra] = unicode(extras[extra]) + return extras + def date_to_db(value, context): try: value = field_types.DateType.form_to_db(value) diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 499c897da3f..e27dbb8ef58 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -10,7 +10,6 @@ from ckan.logic.validators import (package_id_not_changed, package_id_exists, package_id_or_name_exists, - extras_unicode_convert, name_validator, package_name_validator, package_version_validator, @@ -60,6 +59,7 @@ convert_group_name_or_id_to_id, convert_to_json_if_string, remove_whitespace, + extras_unicode_convert, ) from formencode.validators import OneOf import ckan.model diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 26bb2fd3b95..800e17e59ba 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -307,11 +307,6 @@ def object_id_validator(key, activity_dict, errors, context): raise Invalid('There is no object_id validator for ' 'activity type "%s"' % activity_type) -def extras_unicode_convert(extras, context): - for extra in extras: - extras[extra] = unicode(extras[extra]) - return extras - name_match = re.compile('[a-z0-9_\-]*$') def name_validator(value, context): '''Return the given value if it's a valid name, otherwise raise Invalid. From dce2b5583dcb0f1c6b537e837bf92e1a2a2a20a6 Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Wed, 13 Aug 2014 16:29:40 -0400 Subject: [PATCH 04/10] [#1841] document custom converters and validators --- doc/extensions/adding-custom-fields.rst | 103 +++++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/doc/extensions/adding-custom-fields.rst b/doc/extensions/adding-custom-fields.rst index f7a5131f737..286ff9ae0b2 100644 --- a/doc/extensions/adding-custom-fields.rst +++ b/doc/extensions/adding-custom-fields.rst @@ -32,8 +32,8 @@ For example, the schemas can allow optional values by using the :func:`~ckan.lib.navl.validators.ignore_missing` validator or check that a dataset exists using :func:`~ckan.logic.validators.package_id_exists`. A list of available validators and converters can be found at the :doc:`validators` -and :doc:`converters`. You can also define your own validators for custom -validation. +and :doc:`converters`. You can also define your own +:ref:`custom-validators`. We will be customizing these schemas to add our additional fields. The :py:class:`~ckan.plugins.interfaces.IDatasetForm` interface allows us to @@ -189,6 +189,105 @@ with: :start-after: p.implements(p.IDatasetForm) :end-before: def show_package_schema(self): + +.. _custom-validators: + +Custom validators and converters +-------------------------------- + +You may define custom validators and converters in your extensions and +you can share converters and validators between extensions by registering +them with either the :py:class:`~ckan.plugins.interfaces.IValidators` or +:py:class:`~ckan.plugins.interfaces.IConverters` interfaces. + +Any of the following objects may be used as validators/converters as part +of a custom dataset, group or organization schema. CKAN's validation +code will check for and attempt to use them in this order: + +1. a `formencode Validator class `_ (not discussed) + +2. a formencode Validator instance (not discussed) + +3. a callable object taking a single parameter: ``validator(value)`` + +4. a callable object taking four parameters: + ``validator(key, converted_data, errors, context)`` + +5. a callable object taking two parameters + ``validator(value, context)`` + + +``validator(value)`` +^^^^^^^^^^^^^^^^^^^^ + +The simplest form of validator is a callable taking a single +parameter. For example:: + + from ckan.plugins.toolkit import Invalid + + def starts_with_b(value): + if not value.startswith('b'): + raise Invalid("Doesn't start with b") + return value + +The ``starts_with_b`` validator will allow only values starting with 'b', +and cause a +validation error otherwise. On a web form this validation error would +appear next to the field to which the validator was applied. + +``return value`` must be used by validators in this form to when accepting data +or the value will be converted to None. This makes this form useful +for converters as well, because the return value will +replace the field value passed:: + + def embiggen(value): + return value.upper() + +The ``embiggen`` converter will convert values passed to all-uppercase. + + +``validator(value, context)`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Validators and converters that need access to the database or information +about the user may be written as a callable taking two parameters. +``context['session']`` is the sqlalchemy session object and +``context['user']`` is the username of the logged-in user:: + + from ckan.plugins.toolkit import Invalid + + def fred_only(value, context): + if value and context['user'] != 'fred': + raise Invalid('only fred may set this value') + return value + +Otherwise this is the same as the single-parameter form above. + + +``validator(key, flattened_data, errors, context)`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Validators and converters that need to access or update fields other than +the one they are applied to may be written as a callable taking four +parameters. ``context`` is the same value passed to the two-parameter +form above. + +This form of validator or converter is passed the all the fields and +errors for all the fields in a "flattened" form. The validator must fetch +values from ``flattened_data`` and converters may replace values in +``flattened_data``. + +``key`` is the flattened key for the field to which this validator was +applied. For example ``('notes',)`` for the dataset notes field or +``('resources', 0, 'url')`` for the url of the first resource of the dataset. +These flattened keys are used in the ``flattened_data`` and ``errors`` +dicts passed. + +Note that this form is tricky to use because some of the values in +``flattened_data`` will have had validators and converters applied +but other fields won't. + + Tag vocabularies ---------------- If you need to add a custom field where the input options are restricted to a From 03b9d5d3489754783de31ee81afa9851c6201a64 Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Wed, 13 Aug 2014 17:34:32 -0400 Subject: [PATCH 05/10] [#1841] fix grammar, add note about __before, __after validation --- doc/extensions/adding-custom-fields.rst | 40 ++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/doc/extensions/adding-custom-fields.rst b/doc/extensions/adding-custom-fields.rst index 286ff9ae0b2..c64bf01bd41 100644 --- a/doc/extensions/adding-custom-fields.rst +++ b/doc/extensions/adding-custom-fields.rst @@ -197,8 +197,8 @@ Custom validators and converters You may define custom validators and converters in your extensions and you can share converters and validators between extensions by registering -them with either the :py:class:`~ckan.plugins.interfaces.IValidators` or -:py:class:`~ckan.plugins.interfaces.IConverters` interfaces. +them with the :py:class:`~ckan.plugins.interfaces.IValidators` or +:py:class:`~ckan.plugins.interfaces.IConverters` interface. Any of the following objects may be used as validators/converters as part of a custom dataset, group or organization schema. CKAN's validation @@ -211,7 +211,7 @@ code will check for and attempt to use them in this order: 3. a callable object taking a single parameter: ``validator(value)`` 4. a callable object taking four parameters: - ``validator(key, converted_data, errors, context)`` + ``validator(key, flattened_data, errors, context)`` 5. a callable object taking two parameters ``validator(value, context)`` @@ -230,13 +230,13 @@ parameter. For example:: raise Invalid("Doesn't start with b") return value -The ``starts_with_b`` validator will allow only values starting with 'b', -and cause a -validation error otherwise. On a web form this validation error would +The ``starts_with_b`` validator causes a validation error for values +not starting with 'b'. +On a web form this validation error would appear next to the field to which the validator was applied. -``return value`` must be used by validators in this form to when accepting data -or the value will be converted to None. This makes this form useful +``return value`` must be used by validators when accepting data +or the value will be converted to None. This form is useful for converters as well, because the return value will replace the field value passed:: @@ -267,25 +267,31 @@ Otherwise this is the same as the single-parameter form above. ``validator(key, flattened_data, errors, context)`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Validators and converters that need to access or update fields other than -the one they are applied to may be written as a callable taking four -parameters. ``context`` is the same value passed to the two-parameter -form above. +Validators and converters that need to access or update multiple fields +may be written as a callable taking four parameters. This form of validator or converter is passed the all the fields and -errors for all the fields in a "flattened" form. The validator must fetch +errors in a "flattened" form. Validator must fetch values from ``flattened_data`` and converters may replace values in -``flattened_data``. +``flattened_data``. The return value from this function is ignored. ``key`` is the flattened key for the field to which this validator was applied. For example ``('notes',)`` for the dataset notes field or ``('resources', 0, 'url')`` for the url of the first resource of the dataset. -These flattened keys are used in the ``flattened_data`` and ``errors`` +These flattened keys are the same in both the ``flattened_data`` and ``errors`` dicts passed. -Note that this form is tricky to use because some of the values in +``errors`` contains lists of validation errors for each field. + +``context`` is the same value passed to the two-parameter +form above. + +Note that this form can be tricky to use because some of the values in ``flattened_data`` will have had validators and converters applied -but other fields won't. +but other fields won't. You may add this type of validator to the +special schema fields ``'__before'`` or ``'__after'`` to have them +run before or after all the other validation takes place to avoid +the problem of working with partially-validated data. Tag vocabularies From adb2ba6ce783bd1acb717f81d113cb7ffceebdaa Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Thu, 14 Aug 2014 14:24:04 -0400 Subject: [PATCH 06/10] [#1841] tests and fixes --- ckan/logic/__init__.py | 8 ++-- .../__init__.py | 0 .../example_ivalidators_iconverters/plugin.py | 28 ++++++++++++++ .../tests/__init__.py | 0 .../tests/test_ivalidators_iconverters.py | 38 +++++++++++++++++++ setup.py | 2 + 6 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 ckanext/example_ivalidators_iconverters/__init__.py create mode 100644 ckanext/example_ivalidators_iconverters/plugin.py create mode 100644 ckanext/example_ivalidators_iconverters/tests/__init__.py create mode 100644 ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py index f9971be6166..82b5cbdf1b1 100644 --- a/ckan/logic/__init__.py +++ b/ckan/logic/__init__.py @@ -630,13 +630,13 @@ def get_validator(validator): _validators_cache.update({'OneOf': formencode.validators.OneOf}) for plugin in p.PluginImplementations(p.IValidators): - for name, validator in plugin.get_validators().items(): + for name, fn in plugin.get_validators().items(): if name in _validators_cache: raise NameConflict( 'The validator %r is already defined' % (name,) ) log.debug('Validator function {0} from plugin {1} was inserted'.format(name, plugin.name)) - _validators_cache[name] = validator + _validators_cache[name] = fn try: return _validators_cache[validator] except KeyError: @@ -677,13 +677,13 @@ def get_converter(converter): _converters_cache.update(converters) for plugin in p.PluginImplementations(p.IConverters): - for name, converter in plugin.get_converters().items(): + for name, fn in plugin.get_converters().items(): if name in _converters_cache: raise NameConflict( 'The converter %r is already defined' % (name,) ) log.debug('Converter function {0} from plugin {1} was inserted'.format(name, plugin.name)) - _converters_cache[name] = validator + _converters_cache[name] = fn try: return _converters_cache[converter] except KeyError: diff --git a/ckanext/example_ivalidators_iconverters/__init__.py b/ckanext/example_ivalidators_iconverters/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckanext/example_ivalidators_iconverters/plugin.py b/ckanext/example_ivalidators_iconverters/plugin.py new file mode 100644 index 00000000000..50932445d2a --- /dev/null +++ b/ckanext/example_ivalidators_iconverters/plugin.py @@ -0,0 +1,28 @@ +from ckan.plugins import toolkit +from ckan import plugins + +class ExampleIValidatorsPlugin(plugins.SingletonPlugin): + plugins.implements(plugins.IValidators) + + def get_validators(self): + return { + 'equals_fortytwo': equals_fortytwo, + } + + +class ExampleIConvertersPlugin(plugins.SingletonPlugin): + plugins.implements(plugins.IConverters) + + def get_converters(self): + return { + 'negate': negate, + } + +def equals_fortytwo(value): + if value != 42: + raise toolkit.Invalid('not 42') + return value + + +def negate(value): + return -value diff --git a/ckanext/example_ivalidators_iconverters/tests/__init__.py b/ckanext/example_ivalidators_iconverters/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py b/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py new file mode 100644 index 00000000000..3bb82e04828 --- /dev/null +++ b/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py @@ -0,0 +1,38 @@ +from nose.tools import assert_equals, assert_raises +import pylons.config as config + +from ckan.plugins import toolkit +from ckan import plugins + + +class TestIValidators(object): + @classmethod + def setup_class(cls): + plugins.load('example_ivalidators') + + @classmethod + def teardown_class(cls): + plugins.unload('example_ivalidators') + + def test_custom_validator_validates(self): + v = toolkit.get_validator('equals_fortytwo') + assert_raises(toolkit.Invalid, v, 41) + + def test_custom_validator_passes(self): + v = toolkit.get_validator('equals_fortytwo') + assert_equals(v(42), 42) + + +class TestIConverters(object): + @classmethod + def setup_class(cls): + plugins.load('example_iconverters') + + @classmethod + def teardown_class(cls): + plugins.unload('example_iconverters') + + def test_custom_converter_converts(self): + c = toolkit.get_converter('negate') + assert_equals(c(19), -19) + diff --git a/setup.py b/setup.py index 7d271e5a0e8..7ffb49adbf9 100644 --- a/setup.py +++ b/setup.py @@ -107,6 +107,8 @@ 'example_theme_v20_pubsub = ckanext.example_theme.v20_pubsub.plugin:ExampleThemePlugin', 'example_theme_v21_custom_jquery_plugin = ckanext.example_theme.v21_custom_jquery_plugin.plugin:ExampleThemePlugin', 'example_theme_custom_config_setting = ckanext.example_theme.custom_config_setting.plugin:ExampleThemePlugin', + 'example_ivalidators = ckanext.example_ivalidators_iconverters.plugin:ExampleIValidatorsPlugin', + 'example_iconverters = ckanext.example_ivalidators_iconverters.plugin:ExampleIConvertersPlugin', ], 'ckan.system_plugins': [ 'domain_object_mods = ckan.model.modification:DomainObjectModificationExtension', From 878be6695bab91a6e220b05c42f95de8b6c734ac Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Thu, 14 Aug 2014 15:05:21 -0400 Subject: [PATCH 07/10] [#1841] fix tests, pep8 --- ckanext/example_ivalidators_iconverters/plugin.py | 2 ++ .../tests/test_ivalidators_iconverters.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ckanext/example_ivalidators_iconverters/plugin.py b/ckanext/example_ivalidators_iconverters/plugin.py index 50932445d2a..01d9223be8a 100644 --- a/ckanext/example_ivalidators_iconverters/plugin.py +++ b/ckanext/example_ivalidators_iconverters/plugin.py @@ -1,6 +1,7 @@ from ckan.plugins import toolkit from ckan import plugins + class ExampleIValidatorsPlugin(plugins.SingletonPlugin): plugins.implements(plugins.IValidators) @@ -18,6 +19,7 @@ def get_converters(self): 'negate': negate, } + def equals_fortytwo(value): if value != 42: raise toolkit.Invalid('not 42') diff --git a/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py b/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py index 3bb82e04828..ae455b5e852 100644 --- a/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py +++ b/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py @@ -2,6 +2,7 @@ import pylons.config as config from ckan.plugins import toolkit +from ckan.logic import clear_converters_cache, clear_validators_cache from ckan import plugins @@ -9,6 +10,7 @@ class TestIValidators(object): @classmethod def setup_class(cls): plugins.load('example_ivalidators') + clear_validators_cache() @classmethod def teardown_class(cls): @@ -27,6 +29,7 @@ class TestIConverters(object): @classmethod def setup_class(cls): plugins.load('example_iconverters') + clear_converters_cache() @classmethod def teardown_class(cls): @@ -35,4 +38,3 @@ def teardown_class(cls): def test_custom_converter_converts(self): c = toolkit.get_converter('negate') assert_equals(c(19), -19) - From 08a1fb34e6e173846c9260d1278397d6e5cb8840 Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Thu, 14 Aug 2014 16:04:21 -0400 Subject: [PATCH 08/10] [#1841] make plugins.toolkit more like a normal module --- ckan/exceptions.py | 8 ++++++++ ckan/plugins/__init__.py | 5 +---- ckan/plugins/toolkit.py | 35 ++++++++++++++++------------------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/ckan/exceptions.py b/ckan/exceptions.py index 40bea2e8c29..1fc407029f6 100644 --- a/ckan/exceptions.py +++ b/ckan/exceptions.py @@ -6,3 +6,11 @@ class EmptyRevisionException(CkanException): class CkanUrlException(Exception): pass + +class CkanVersionException(Exception): + '''Exception raised by + :py:func:`~ckan.plugins.toolkit.requires_ckan_version` if the required CKAN + version is not available. + + ''' + pass diff --git a/ckan/plugins/__init__.py b/ckan/plugins/__init__.py index 719a24a4c12..1a84eee628e 100644 --- a/ckan/plugins/__init__.py +++ b/ckan/plugins/__init__.py @@ -1,7 +1,4 @@ from ckan.plugins.core import * from ckan.plugins.interfaces import * -# Expose the toolkit object without doing an import * -import toolkit as _toolkit -toolkit = _toolkit.toolkit -del _toolkit +import toolkit diff --git a/ckan/plugins/toolkit.py b/ckan/plugins/toolkit.py index e436c82a582..90c95aef592 100644 --- a/ckan/plugins/toolkit.py +++ b/ckan/plugins/toolkit.py @@ -1,20 +1,4 @@ -import inspect -import os -import re - -import paste.deploy.converters as converters -import webhelpers.html.tags - -__all__ = ['toolkit'] - - -class CkanVersionException(Exception): - '''Exception raised by - :py:func:`~ckan.plugins.toolkit.requires_ckan_version` if the required CKAN - version is not available. - - ''' - pass +import sys class _Toolkit(object): @@ -102,7 +86,12 @@ def _initialize(self): import ckan.lib.plugins as lib_plugins import ckan.common as common import ckan.lib.datapreview as datapreview + from ckan.exceptions import CkanVersionException + + from paste.deploy import converters import pylons + import webhelpers.html.tags + # Allow class access to these modules self.__class__.ckan = ckan @@ -251,6 +240,9 @@ def _add_public_directory(cls, config, relative_path): @classmethod def _add_served_directory(cls, config, relative_path, config_var): ''' Add extra public/template directories to config. ''' + import inspect + import os + assert config_var in ('extra_template_paths', 'extra_public_paths') # we want the filename that of the function caller but they will # have used one of the available helper functions @@ -275,6 +267,9 @@ def _add_resource(cls, path, name): See :doc:`/theming/index` for more details. ''' + import inspect + import os + # we want the filename that of the function caller but they will # have used one of the available helper functions frame, filename, line_number, function_name, lines, index =\ @@ -289,6 +284,7 @@ def _add_resource(cls, path, name): def _version_str_2_list(cls, v_str): ''' convert a version string into a list of ints eg 1.6.1b --> [1, 6, 1] ''' + import re v_str = re.sub(r'[^0-9.]', '', v_str) return [int(part) for part in v_str.split('.')] @@ -347,6 +343,7 @@ def _requires_ckan_version(cls, min_version, max_version=None): :type max_version: string ''' + from ckan.exceptions import CkanVersionException if not cls._check_ckan_version(min_version=min_version, max_version=max_version): if not max_version: @@ -373,5 +370,5 @@ def __dir__(self): return sorted(self._toolkit.keys()) -toolkit = _Toolkit() -del _Toolkit +# https://mail.python.org/pipermail/python-ideas/2012-May/014969.html +sys.modules[__name__] = _Toolkit() From 1771a234ccace0bce1cc2e5f5256b891dc279ed2 Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Thu, 14 Aug 2014 16:15:19 -0400 Subject: [PATCH 09/10] [#1841] import plugins.toolkit like a normal module --- ckanext/example_ivalidators_iconverters/plugin.py | 4 ++-- .../tests/test_ivalidators_iconverters.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ckanext/example_ivalidators_iconverters/plugin.py b/ckanext/example_ivalidators_iconverters/plugin.py index 01d9223be8a..05481782370 100644 --- a/ckanext/example_ivalidators_iconverters/plugin.py +++ b/ckanext/example_ivalidators_iconverters/plugin.py @@ -1,4 +1,4 @@ -from ckan.plugins import toolkit +from ckan.plugins.toolkit import Invalid from ckan import plugins @@ -22,7 +22,7 @@ def get_converters(self): def equals_fortytwo(value): if value != 42: - raise toolkit.Invalid('not 42') + raise Invalid('not 42') return value diff --git a/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py b/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py index ae455b5e852..d0feb8db4f4 100644 --- a/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py +++ b/ckanext/example_ivalidators_iconverters/tests/test_ivalidators_iconverters.py @@ -1,7 +1,7 @@ from nose.tools import assert_equals, assert_raises import pylons.config as config -from ckan.plugins import toolkit +from ckan.plugins.toolkit import get_validator, get_converter, Invalid from ckan.logic import clear_converters_cache, clear_validators_cache from ckan import plugins @@ -17,11 +17,11 @@ def teardown_class(cls): plugins.unload('example_ivalidators') def test_custom_validator_validates(self): - v = toolkit.get_validator('equals_fortytwo') - assert_raises(toolkit.Invalid, v, 41) + v = get_validator('equals_fortytwo') + assert_raises(Invalid, v, 41) def test_custom_validator_passes(self): - v = toolkit.get_validator('equals_fortytwo') + v = get_validator('equals_fortytwo') assert_equals(v(42), 42) @@ -36,5 +36,5 @@ def teardown_class(cls): plugins.unload('example_iconverters') def test_custom_converter_converts(self): - c = toolkit.get_converter('negate') + c = get_converter('negate') assert_equals(c(19), -19) From de9f53d8b6dbffd6ecd620e128eb92c94827b485 Mon Sep 17 00:00:00 2001 From: Ian Ward Date: Thu, 14 Aug 2014 16:35:28 -0400 Subject: [PATCH 10/10] [#1841] better docstrings for IConverters, IValidators --- ckan/plugins/interfaces.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index b33ac154db7..4c938915a44 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -600,23 +600,40 @@ def get_actions(self): class IValidators(Interface): """ - Allow adding of validators to be returned by ``get_validator()``. + Add extra validators to be returned by + :py:func:`ckan.plugins.toolkit.get_validator`. """ def get_validators(self): - """ - Should return a dict, the keys being the name of the validator - function and the values being the functions themselves. + """Return the validator functions provided by this plugin. + + Return a dictionary mapping validator names (strings) to + validator functions. For example:: + + {'valid_shoe_size': shoe_size_validator, + 'valid_hair_color': hair_color_validator} + + These validator functions would then be available when a + plugin calls :py:func:`ckan.plugins.toolkit.get_validator`. """ class IConverters(Interface): """ - Allow adding of converters to be returned by ``get_converter()``. + Add extra converters to be returned by + :py:func:`ckan.plugins.toolkit.get_converter`. + """ def get_converters(self): - """ - Should return a dict, the keys being the name of the converter - function and the values being the functions themselves. + """Return the converter functions provided by this plugin. + + Return a dictionary mapping converter names (strings) to + converter functions. For example:: + + {'truncate_words': word_truncator, + 'insert_links': link_inserter} + + These converter functions would then be available when a + plugin calls :py:func:`ckan.plugins.toolkit.get_converter`. """