Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/source/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ A simple utility to import something by its string name.

.. autofunction:: import_item

A way to expand the signature of the ``HasTraits`` class constructor. This enables auto-completion of trait-names in IPython and xeus-python when having Jedi>=0.15 by adding trait names with their default values in the constructor signature.
Example:

.. code:: Python

from inspect import signature

from traitlets import HasTraits, Int, Unicode, signature_has_traits

@signature_has_traits
class Foo(HasTraits):
number1 = Int()
number2 = Int()
value = Unicode('Hello')

def __init__(self, arg1, **kwargs):
self.arg1 = arg1

super(Foo, self).__init__(**kwargs)

print(signature(Foo)) # <Signature (arg1, *, number1=0, number2=0, value='Hello', **kwargs)>

.. autofunction:: signature_has_traits

Links
-----

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
install_requires = setuptools_args['install_requires'] = [
'ipython_genutils',
'six',
'funcsigs',
]

extras_require = setuptools_args['extras_require'] = {
Expand Down
1 change: 1 addition & 0 deletions traitlets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from . import traitlets
from .traitlets import *
from .utils.importstring import import_item
from .utils.decorators import signature_has_traits
from ._version import version_info, __version__


Expand Down
83 changes: 83 additions & 0 deletions traitlets/utils/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Useful decorators for Traitlets users."""

import copy

try:
from inspect import Signature, Parameter, signature
except ImportError:
from funcsigs import Signature, Parameter, signature

from ..traitlets import Undefined


def _get_default(value):
"""Get default argument value, given the trait default value."""
return Parameter.empty if value == Undefined else value


def signature_has_traits(cls):
"""Return a decorated class with a constructor signature that contain Trait names as kwargs."""
traits = [
(name, _get_default(value.default_value))
for name, value in cls.class_traits().items()
if not name.startswith('_')
]

# Taking the __init__ signature, as the cls signature is not initialized yet
old_signature = signature(cls.__init__)
old_parameter_names = list(old_signature.parameters)

old_positional_parameters = []
old_var_positional_parameter = None # This won't be None if the old signature contains *args
old_keyword_only_parameters = []
old_var_keyword_parameter = None # This won't be None if the old signature contains **kwargs

for parameter_name in old_signature.parameters:
# Copy the parameter
parameter = copy.copy(old_signature.parameters[parameter_name])

if parameter.kind is Parameter.POSITIONAL_ONLY or parameter.kind is Parameter.POSITIONAL_OR_KEYWORD:
old_positional_parameters.append(parameter)

elif parameter.kind is Parameter.VAR_POSITIONAL:
old_var_positional_parameter = parameter

elif parameter.kind is Parameter.KEYWORD_ONLY:
old_keyword_only_parameters.append(parameter)

elif parameter.kind is Parameter.VAR_KEYWORD:
old_var_keyword_parameter = parameter

# Unfortunately, if the old signature does not contain **kwargs, we can't do anything,
# because it can't accept traits as keyword arguments
if old_var_keyword_parameter is None:
raise RuntimeError(
'The {} constructor does not take **kwargs, which means that the signature can not be expanded with trait names'
.format(cls)
)

new_parameters = []

# Append the old positional parameters (except `self` which is the first parameter)
new_parameters += old_positional_parameters[1:]

# Append *args if the old signature had it
if old_var_positional_parameter is not None:
new_parameters.append(old_var_positional_parameter)

# Append the old keyword only parameters
new_parameters += old_keyword_only_parameters

# Append trait names as keyword only parameters in the signature
new_parameters += [
Parameter(name, kind=Parameter.KEYWORD_ONLY, default=default)
for name, default in traits
if name not in old_parameter_names
]

# Append **kwargs
new_parameters.append(old_var_keyword_parameter)

cls.__signature__ = Signature(new_parameters)

return cls
141 changes: 141 additions & 0 deletions traitlets/utils/tests/test_decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from unittest import TestCase
try:
from inspect import Signature, Parameter, signature
except ImportError:
from funcsigs import Signature, Parameter, signature

from ...traitlets import HasTraits, Int, Unicode

from ..decorators import signature_has_traits


class TestExpandSignature(TestCase):

def test_no_init(self):
@signature_has_traits
class Foo(HasTraits):
number1 = Int()
number2 = Int()
value = Unicode('Hello')

parameters = signature(Foo).parameters
parameter_names = list(parameters)

self.assertIs(parameters['args'].kind, Parameter.VAR_POSITIONAL)
self.assertEqual('args', parameter_names[0])

self.assertIs(parameters['number1'].kind, Parameter.KEYWORD_ONLY)
self.assertIs(parameters['number2'].kind, Parameter.KEYWORD_ONLY)
self.assertIs(parameters['value'].kind, Parameter.KEYWORD_ONLY)

self.assertIs(parameters['kwargs'].kind, Parameter.VAR_KEYWORD)
self.assertEqual('kwargs', parameter_names[-1])

f = Foo(number1=32, value='World')
self.assertEqual(f.number1, 32)
self.assertEqual(f.number2, 0)
self.assertEqual(f.value, 'World')

def test_partial_init(self):
@signature_has_traits
class Foo(HasTraits):
number1 = Int()
number2 = Int()
value = Unicode('Hello')

def __init__(self, arg1, **kwargs):
self.arg1 = arg1

super(Foo, self).__init__(**kwargs)

parameters = signature(Foo).parameters
parameter_names = list(parameters)

self.assertIs(parameters['arg1'].kind, Parameter.POSITIONAL_OR_KEYWORD)
self.assertEqual('arg1', parameter_names[0])

self.assertIs(parameters['number1'].kind, Parameter.KEYWORD_ONLY)
self.assertIs(parameters['number2'].kind, Parameter.KEYWORD_ONLY)
self.assertIs(parameters['value'].kind, Parameter.KEYWORD_ONLY)

self.assertIs(parameters['kwargs'].kind, Parameter.VAR_KEYWORD)
self.assertEqual('kwargs', parameter_names[-1])

f = Foo(1, number1=32, value='World')
self.assertEqual(f.arg1, 1)
self.assertEqual(f.number1, 32)
self.assertEqual(f.number2, 0)
self.assertEqual(f.value, 'World')

def test_duplicate_init(self):
@signature_has_traits
class Foo(HasTraits):
number1 = Int()
number2 = Int()

def __init__(self, number1, **kwargs):
self.test = number1

super(Foo, self).__init__(number1=number1, **kwargs)

parameters = signature(Foo).parameters
parameter_names = list(parameters)

self.assertListEqual(parameter_names, ['number1', 'number2', 'kwargs'])

f = Foo(number1=32, number2=36)
self.assertEqual(f.test, 32)
self.assertEqual(f.number1, 32)
self.assertEqual(f.number2, 36)

def test_full_init(self):
@signature_has_traits
class Foo(HasTraits):
number1 = Int()
number2 = Int()
value = Unicode('Hello')

def __init__(self, arg1, arg2=None, *pos_args, **kw_args):
self.arg1 = arg1
self.arg2 = arg2
self.pos_args = pos_args
self.kw_args = kw_args

super(Foo, self).__init__(*pos_args, **kw_args)

parameters = signature(Foo).parameters
parameter_names = list(parameters)

self.assertIs(parameters['arg1'].kind, Parameter.POSITIONAL_OR_KEYWORD)
self.assertEqual('arg1', parameter_names[0])

self.assertIs(parameters['arg2'].kind, Parameter.POSITIONAL_OR_KEYWORD)
self.assertEqual('arg2', parameter_names[1])

self.assertIs(parameters['pos_args'].kind, Parameter.VAR_POSITIONAL)
self.assertEqual('pos_args', parameter_names[2])

self.assertIs(parameters['number1'].kind, Parameter.KEYWORD_ONLY)
self.assertIs(parameters['number2'].kind, Parameter.KEYWORD_ONLY)
self.assertIs(parameters['value'].kind, Parameter.KEYWORD_ONLY)

self.assertIs(parameters['kw_args'].kind, Parameter.VAR_KEYWORD)
self.assertEqual('kw_args', parameter_names[-1])

f = Foo(1, 3, 45, 'hey', number1=32, value='World')
self.assertEqual(f.arg1, 1)
self.assertEqual(f.arg2, 3)
self.assertTupleEqual(f.pos_args, (45, 'hey'))
self.assertEqual(f.number1, 32)
self.assertEqual(f.number2, 0)
self.assertEqual(f.value, 'World')

def test_no_kwargs(self):
with self.assertRaises(RuntimeError):
@signature_has_traits
class Foo(HasTraits):
number1 = Int()
number2 = Int()

def __init__(self, arg1, arg2=None):
pass