Skip to content

Commit

Permalink
Merge branch '1841-ivalidators-iconverters'
Browse files Browse the repository at this point in the history
  • Loading branch information
seanh committed Aug 21, 2014
2 parents d59713f + de9f53d commit 925ba6e
Show file tree
Hide file tree
Showing 14 changed files with 275 additions and 34 deletions.
8 changes: 8 additions & 0 deletions ckan/exceptions.py
Expand Up @@ -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
24 changes: 23 additions & 1 deletion ckan/logic/__init__.py
Expand Up @@ -16,6 +16,10 @@
_validate = df.validate


class NameConflict(Exception):
pass


class AttributeDict(dict):
def __getattr__(self, name):
try:
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -624,6 +628,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, 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] = fn
try:
return _validators_cache[validator]
except KeyError:
Expand Down Expand Up @@ -662,6 +675,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, 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] = fn
try:
return _converters_cache[converter]
except KeyError:
Expand Down
5 changes: 5 additions & 0 deletions ckan/logic/converters.py
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion ckan/logic/schema.py
Expand Up @@ -11,7 +11,6 @@
package_id_exists,
package_id_or_name_exists,
resource_id_exists,
extras_unicode_convert,
name_validator,
package_name_validator,
package_version_validator,
Expand Down Expand Up @@ -61,6 +60,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
Expand Down
5 changes: 0 additions & 5 deletions ckan/logic/validators.py
Expand Up @@ -316,11 +316,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.
Expand Down
5 changes: 1 addition & 4 deletions 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
40 changes: 40 additions & 0 deletions ckan/plugins/interfaces.py
Expand Up @@ -13,6 +13,7 @@
'IPackageController', 'IPluginObserver',
'IConfigurable', 'IConfigurer',
'IActions', 'IResourceUrlChange', 'IDatasetForm',
'IValidators', 'IConverters',
'IResourcePreview',
'IResourceView',
'IResourceController',
Expand Down Expand Up @@ -597,6 +598,45 @@ def get_actions(self):
"""


class IValidators(Interface):
"""
Add extra validators to be returned by
:py:func:`ckan.plugins.toolkit.get_validator`.
"""
def get_validators(self):
"""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):
"""
Add extra converters to be returned by
:py:func:`ckan.plugins.toolkit.get_converter`.
"""
def get_converters(self):
"""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`.
"""


class IAuthFunctions(Interface):
'''Override CKAN's authorization functions, or add new auth functions.'''

Expand Down
39 changes: 18 additions & 21 deletions 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):
Expand Down Expand Up @@ -44,8 +28,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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 =\
Expand All @@ -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('.')]

Expand Down Expand Up @@ -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:
Expand All @@ -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()
Empty file.
30 changes: 30 additions & 0 deletions ckanext/example_ivalidators_iconverters/plugin.py
@@ -0,0 +1,30 @@
from ckan.plugins.toolkit import Invalid
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 Invalid('not 42')
return value


def negate(value):
return -value
Empty file.
@@ -0,0 +1,40 @@
from nose.tools import assert_equals, assert_raises
import pylons.config as config

from ckan.plugins.toolkit import get_validator, get_converter, Invalid
from ckan.logic import clear_converters_cache, clear_validators_cache
from ckan import plugins


class TestIValidators(object):
@classmethod
def setup_class(cls):
plugins.load('example_ivalidators')
clear_validators_cache()

@classmethod
def teardown_class(cls):
plugins.unload('example_ivalidators')

def test_custom_validator_validates(self):
v = get_validator('equals_fortytwo')
assert_raises(Invalid, v, 41)

def test_custom_validator_passes(self):
v = get_validator('equals_fortytwo')
assert_equals(v(42), 42)


class TestIConverters(object):
@classmethod
def setup_class(cls):
plugins.load('example_iconverters')
clear_converters_cache()

@classmethod
def teardown_class(cls):
plugins.unload('example_iconverters')

def test_custom_converter_converts(self):
c = get_converter('negate')
assert_equals(c(19), -19)

0 comments on commit 925ba6e

Please sign in to comment.