-
-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #309 from olaurino/experimental-features
support decorating experimental features.
- Loading branch information
Showing
5 changed files
with
411 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -125,3 +125,4 @@ Using `pyvo` | |
registry/index | ||
io/index | ||
auth/index | ||
prototypes/index |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
.. _pyvo-prototypes: | ||
|
||
************************************************** | ||
Prototype Implementations (`pyvo.utils.prototype`) | ||
************************************************** | ||
|
||
This subpackage provides support for prototype standard implementations. | ||
|
||
``PyVO`` implements the IVOA standards. As part of the standard approval process, new features | ||
are proposed and need to be demonstrated before the standard may be approved. ``PyVO`` | ||
may implement features that are not yet part of an approved standard. | ||
Such features are unstable, as the standard may be subject to reviews and significant changes, until it's finally | ||
approved. | ||
|
||
The ``prototype`` package provides support for such prototypes by means of a decorator | ||
for implementations that are still unstable. The expectation is that they will eventually become | ||
standard at which time the decorator will be removed. | ||
|
||
Users of ``pyvo`` need to explicitly opt-in in order to use such features. If prototype | ||
implementations are accessed without the user explicitly opting in, an exception will be raised. | ||
|
||
.. _pyvo-prototypes-users: | ||
|
||
Activating Prototype Implementations | ||
==================================== | ||
|
||
In order to activate a feature, users need to call the function:: | ||
|
||
activate_features('feature_one', 'feature_two') | ||
|
||
Where the arguments are names of prototype features. If a feature name does not exist, a `~pyvo.utils.prototype.PrototypeWarning` | ||
will be issued, but the call will not fail. If no arguments are provided, then all features are enabled. | ||
|
||
.. _pyvo-prototypes-developers: | ||
|
||
Marking Features as Experimental | ||
================================ | ||
The design restricts the possible usage of the decorator, which needs to always be called | ||
with a single argument being the name of the corresponding feature. More arguments are allowed | ||
but will be ignored. If the decorator is not used with the correct | ||
``@prototype_feature("feature-name")`` invocation, the code will error as soon as the class is | ||
imported. | ||
|
||
The decorator can be used to tag individual functions or methods:: | ||
|
||
@prototype_feature('a-feature') | ||
def i_am_a_prototype(*arg, **kwargs): | ||
pass | ||
|
||
In this case, a single function or method is tagged as part of the ``a-feature`` prototype feature. If the feature | ||
has a URL defined (see :ref:`pyvo-prototypes-registry` below). | ||
|
||
Alternatively, a class can be marked as belonging to a feature. All public methods will be marked as part of the | ||
prototype implementation. Protected, private, and *dunder* methods (i.e. any method starting with | ||
an underscore) will be ignored. The reason is that the class might be instantiated by some mediator before the | ||
user can call (and more importantly not call) a higher level facade:: | ||
|
||
@prototype_feature('a-feature') | ||
class SomeFeatureClass: | ||
def method(self): | ||
pass | ||
|
||
@staticmethod | ||
def static(): | ||
pass | ||
|
||
def __ignore__(self): | ||
pass | ||
|
||
Any number of classes and functions can belong to a single feature, and individual methods can be tagged | ||
in a class rather than the class itself. | ||
|
||
.. _pyvo-prototypes-registry: | ||
|
||
Feature Registry | ||
================ | ||
|
||
The feature registry is a static ``features`` dictionary in the `~pyvo.utils.prototype` package. The key is the name | ||
of the feature and the value is an instance of the `~pyvo.utils.prototype.Feature` class. This class is responsible for determining | ||
whether an instance should error or not, and to format an error message if it's not. While the current implementation | ||
of the ``Feature`` class is simple, future requirements might lead to other implementations with more complex logic or | ||
additional documentation elements. | ||
|
||
.. _pyvo-prototypes-api: | ||
|
||
Reference/API | ||
============= | ||
|
||
.. automodapi:: pyvo.utils.prototype |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
from .compat import * | ||
from .prototype import prototype_feature, activate_features |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import inspect | ||
import warnings | ||
from dataclasses import dataclass | ||
from functools import wraps | ||
from typing import Dict, Iterable | ||
|
||
from pyvo.dal.exceptions import PyvoUserWarning | ||
|
||
__all__ = ['features', 'prototype_feature', 'activate_features', 'Feature', 'PrototypeWarning', 'PrototypeError'] | ||
|
||
features: Dict[str, "Feature"] = { | ||
|
||
} | ||
|
||
|
||
def prototype_feature(*args): | ||
""" | ||
Decorator for functions and classes that implement unstable standards which haven't been approved yet. | ||
The decorator can be used to tag individual functions or methods. | ||
Please refer to the user documentation for details. | ||
Parameters | ||
---------- | ||
args: iterable of arguments. | ||
Currently, the decorator must always be called with one and only one argument, a string representing | ||
the feature's name associated with the decorated class or functions. Additional arguments will be ignored, | ||
while using the decorator without any arguments will result in a ``PrototypeError`` error. | ||
Returns | ||
------- | ||
The class or function it decorates, which will be associated to the feature provided as argument. | ||
""" | ||
feature_name = _parse_args(*args) | ||
decorator = _make_decorator(feature_name) | ||
return decorator | ||
|
||
|
||
def activate_features(*feature_names: Iterable[str]): | ||
""" | ||
Activate one or more prototype features. | ||
Parameters | ||
---------- | ||
feature_names: Iterable[str] | ||
An arbitrary number of feature names. If a feature with that name does not exist, a `PrototypeWarning` will | ||
be issued. If no arguments are provided, all features will be activated | ||
Returns | ||
------- | ||
""" | ||
names = feature_names or set(features.keys()) | ||
for name in names: | ||
if not _validate(name): | ||
continue | ||
features[name].on = True | ||
|
||
|
||
@dataclass | ||
class Feature: | ||
""" | ||
A prototype feature implementing a standard that is currently in the process of being approved, but that might | ||
change as a result of the approval process. A Feature must have a name. Optionally, a feature may have a *url* | ||
that is displayed to the user in case a feature is used without the user explicitly opting in on its usage. | ||
The URL is expected to contain more information about the standard and its state in the approval process. | ||
""" | ||
name: str | ||
url: str = '' | ||
on: bool = False | ||
|
||
def should_error(self): | ||
""" | ||
Should accessing this feature fail? | ||
Returns | ||
------- | ||
bool Whether accessing this feature should result in an error. | ||
""" | ||
return not self.on | ||
|
||
def error(self, function_name): | ||
""" | ||
Format an error message when the feature is being accesses without the user having opted in its usage. | ||
This function will be used as a callback when an error message needs to be displayed to the user, with the | ||
function name that was accessed as an argument. Extensions of this class may have additional information to | ||
display. | ||
Parameters | ||
---------- | ||
function_name: str | ||
The name of the function associated to this feature and that the user called. | ||
Returns | ||
------- | ||
str: The error message to be displayed to the user. | ||
""" | ||
message = f'{function_name} is part of a prototype feature ({self.name}) that has not been activated. ' \ | ||
f'For information about prototype features please refer to ' \ | ||
f'https://pyvo.readthedocs.io/en/latest/prototypes .' | ||
if self.url: | ||
message += f' For more information about the {self.name} feature please visit {self.url}.' | ||
message += f' To suppress this error and enable the feature use `pyvo.utils.activate_features(\'{self.name}\')`' | ||
return message | ||
|
||
|
||
class PrototypeError(Exception): | ||
pass | ||
|
||
|
||
class PrototypeWarning(PyvoUserWarning): | ||
pass | ||
|
||
|
||
def _parse_args(*args): | ||
if not args or callable(args[0]): | ||
raise PrototypeError("The `prototype_feature` decorator must always be called with the feature name as an " | ||
"argument") | ||
return args[0] | ||
|
||
|
||
def _make_decorator(feature_name): | ||
|
||
def decorator(decorated): | ||
if inspect.isfunction(decorated): | ||
return _make_wrapper(feature_name, decorated) | ||
|
||
if inspect.isclass(decorated): | ||
method_infos = inspect.getmembers(decorated, predicate=_should_wrap) | ||
_wrap_class_methods(decorated, method_infos, feature_name) | ||
|
||
return decorated | ||
|
||
return decorator | ||
|
||
|
||
def _validate(feature_name): | ||
if feature_name not in features: | ||
warnings.warn(f'No such feature "{feature_name}"', category=PrototypeWarning) | ||
return False | ||
return True | ||
|
||
|
||
def _warn_or_raise(function, feature_name): | ||
_validate(feature_name) | ||
feature = features[feature_name] | ||
|
||
if feature.should_error(): | ||
raise PrototypeError(feature.error(function.__name__)) | ||
|
||
|
||
def _should_wrap(member): | ||
return inspect.isfunction(member) and not member.__name__.startswith('_') | ||
|
||
|
||
def _wrap_class_methods(decorated_class, method_infos, feature_name): | ||
for method_info in method_infos: | ||
setattr(decorated_class, method_info[0], _make_wrapper(feature_name, method_info[1])) | ||
|
||
|
||
def _make_wrapper(feature_name, function): | ||
@wraps(function) | ||
def wrapper(*args, **kwargs): | ||
_warn_or_raise(function, feature_name) | ||
return function(*args, **kwargs) | ||
return wrapper |
Oops, something went wrong.