Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Don’t directly import numpy and add gen_types decorator #966

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
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 param/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,3 +612,27 @@ def async_executor(func):
task.add_done_callback(_running_tasks.discard)
else:
event_loop.run_until_complete(func())

class _GeneratorIsMeta(type):
def __instancecheck__(cls, inst):
return isinstance(inst, tuple(cls.types()))

def __subclasscheck__(cls, sub):
return issubclass(sub, tuple(cls.types()))

def __iter__(cls):
yield from cls.types()

class _GeneratorIs(metaclass=_GeneratorIsMeta):
@classmethod
def __iter__(cls):
yield from cls.types()

def gen_types(gen_func):
"""
Decorator which takes a generator function which yields difference types
make it so it can be called with isinstance and issubclass."""
if not inspect.isgeneratorfunction(gen_func):
msg = "gen_types decorator can only be applied to generator"
raise TypeError(msg)
return type(gen_func.__name__, (_GeneratorIs,), {"types": staticmethod(gen_func)})
55 changes: 32 additions & 23 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
import logging
import numbers
import operator
import random
import re
import sys
import types
import typing
import warnings
from inspect import getfullargspec

# Allow this file to be used standalone if desired, albeit without JSON serialization
try:
Expand Down Expand Up @@ -55,6 +56,7 @@
accept_arguments,
iscoroutinefunction,
descendents,
gen_types,
)

# Ideally setting param_pager would be in __init__.py but param_pager is
Expand All @@ -72,17 +74,18 @@
param_pager = None


from inspect import getfullargspec

dt_types = (dt.datetime, dt.date)
_int_types = (int,)
@gen_types
def _dt_types():
yield dt.datetime
yield dt.date
if np := sys.modules.get("numpy"):
yield np.datetime64

try:
import numpy as np
dt_types = dt_types + (np.datetime64,)
_int_types = _int_types + (np.integer,)
except:
pass
@gen_types
def _int_types():
yield int
if np := sys.modules.get("numpy"):
yield np.integer

VERBOSE = INFO - 1
logging.addLevelName(VERBOSE, "VERBOSE")
Expand Down Expand Up @@ -1715,11 +1718,18 @@ class Comparator:
type(None): operator.eq,
lambda o: hasattr(o, '_infinitely_iterable'): operator.eq, # Time
}
equalities.update({dtt: operator.eq for dtt in dt_types})
gen_equalities = {
_dt_types: operator.eq
}

@classmethod
def is_equal(cls, obj1, obj2):
for eq_type, eq in cls.equalities.items():
equals = cls.equalities.copy()
for gen, op in cls.gen_equalities.items():
for t in gen():
equals[t] = op

for eq_type, eq in equals.items():
try:
are_instances = isinstance(obj1, eq_type) and isinstance(obj2, eq_type)
except TypeError:
Expand Down Expand Up @@ -3805,6 +3815,9 @@ def pprint(val,imports=None, prefix="\n ", settings=[],
elif type(val) in script_repr_reg:
rep = script_repr_reg[type(val)](val,imports,prefix,settings)

elif isinstance(val, _no_script_repr):
rep = None

elif isinstance(val, Parameterized) or (type(val) is type and issubclass(val, Parameterized)):
rep=val.param.pprint(imports=imports, prefix=prefix+" ",
qualify=qualify, unknown_value=unknown_value,
Expand Down Expand Up @@ -3839,17 +3852,13 @@ def container_script_repr(container,imports,prefix,settings):
return rep


def empty_script_repr(*args): # pyflakes:ignore (unused arguments):
return None

try:
@gen_types
def _no_script_repr():
# Suppress scriptrepr for objects not yet having a useful string representation
import numpy
script_repr_reg[random.Random] = empty_script_repr
script_repr_reg[numpy.random.RandomState] = empty_script_repr

except ImportError:
pass # Support added only if those libraries are available
if random := sys.modules.get("random"):
yield random.Random
if npr := sys.modules.get("numpy.random"):
yield npr.RandomState


def function_script_repr(fn,imports,prefix,settings):
Expand Down
37 changes: 19 additions & 18 deletions param/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@
import warnings

from collections import OrderedDict
from collections.abc import Iterable
from contextlib import contextmanager

from .parameterized import (
Parameterized, Parameter, ParameterizedFunction, ParamOverrides, String,
Undefined, get_logger, instance_descriptor, dt_types,
Undefined, get_logger, instance_descriptor, _dt_types,
_int_types, _identity_hook
)
from ._utils import (
Expand Down Expand Up @@ -94,7 +95,7 @@ def guess_param_types(**kwargs):
kws = dict(default=v, constant=True)
if isinstance(v, Parameter):
params[k] = v
elif isinstance(v, dt_types):
elif isinstance(v, _dt_types):
params[k] = Date(**kws)
elif isinstance(v, bool):
params[k] = Boolean(**kws)
Expand All @@ -109,7 +110,7 @@ def guess_param_types(**kwargs):
elif isinstance(v, tuple):
if all(_is_number(el) for el in v):
params[k] = NumericTuple(**kws)
elif all(isinstance(el, dt_types) for el in v) and len(v)==2:
elif len(v) == 2 and all(isinstance(el, _dt_types) for el in v):
params[k] = DateRange(**kws)
else:
params[k] = Tuple(**kws)
Expand Down Expand Up @@ -141,7 +142,7 @@ def parameterized_class(name, params, bases=Parameterized):
Dynamically create a parameterized class with the given name and the
supplied parameters, inheriting from the specified base(s).
"""
if not (isinstance(bases, list) or isinstance(bases, tuple)):
if not isinstance(bases, (list, tuple)):
bases=[bases]
return type(name, tuple(bases), params)

Expand Down Expand Up @@ -917,14 +918,14 @@ def _validate_value(self, val, allow_None):
if self.allow_None and val is None:
return

if not isinstance(val, dt_types) and not (allow_None and val is None):
if not isinstance(val, _dt_types) and not (allow_None and val is None):
raise ValueError(
f"{_validate_error_prefix(self)} only takes datetime and "
f"date types, not {type(val)}."
)

def _validate_step(self, val, step):
if step is not None and not isinstance(step, dt_types):
if step is not None and not isinstance(step, _dt_types):
raise ValueError(
f"{_validate_error_prefix(self, 'step')} can only be None, "
f"a datetime or date type, not {type(step)}."
Expand Down Expand Up @@ -1355,7 +1356,7 @@ class DateRange(Range):
"""

def _validate_bound_type(self, value, position, kind):
if not isinstance(value, dt_types):
if not isinstance(value, _dt_types):
raise ValueError(
f"{_validate_error_prefix(self)} {position} {kind} can only be "
f"None or a date/datetime value, not {type(value)}."
Expand All @@ -1379,7 +1380,7 @@ def _validate_value(self, val, allow_None):
f"not {type(val)}."
)
for n in val:
if isinstance(n, dt_types):
if isinstance(n, _dt_types):
continue
raise ValueError(
f"{_validate_error_prefix(self)} only takes date/datetime "
Expand Down Expand Up @@ -2184,18 +2185,18 @@ def _validate(self, val):
def _validate_class_(self, val, class_, is_instance):
if (val is None and self.allow_None):
return
if isinstance(class_, tuple):
class_name = ('(%s)' % ', '.join(cl.__name__ for cl in class_))
if (is_instance and isinstance(val, class_)) or (not is_instance and issubclass(val, class_)):
return

if isinstance(class_, Iterable):
class_name = ('({})'.format(', '.join(cl.__name__ for cl in class_)))
else:
class_name = class_.__name__
if is_instance:
if not (isinstance(val, class_)):
raise ValueError(
f"{_validate_error_prefix(self)} value must be an instance of {class_name}, not {val!r}.")
else:
if not (issubclass(val, class_)):
raise ValueError(
f"{_validate_error_prefix(self)} value must be a subclass of {class_name}, not {val}.")

raise ValueError(
f"{_validate_error_prefix(self)} value must be "
f"{'an instance' if is_instance else 'a subclass'} of {class_name}, not {val!r}."
)

def get_range(self):
"""
Expand Down
20 changes: 20 additions & 0 deletions tests/testimports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import sys
from subprocess import check_output
from textwrap import dedent


def test_no_blocklist_imports():
check = """\
import sys
import param

blocklist = {"numpy", "IPython", "pandas"}
mods = blocklist & set(sys.modules)

if mods:
print(", ".join(mods), end="")
"""

output = check_output([sys.executable, '-c', dedent(check)])

assert output == b""
20 changes: 19 additions & 1 deletion tests/testutils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import datetime as dt
import os

from collections.abc import Iterable
from functools import partial

import param
import pytest

from param import guess_param_types, resolve_path
from param.parameterized import bothmethod
from param._utils import _is_mutable_container, iscoroutinefunction
from param._utils import _is_mutable_container, iscoroutinefunction, gen_types


try:
Expand Down Expand Up @@ -421,3 +422,20 @@ def test_iscoroutinefunction_asyncgen():
def test_iscoroutinefunction_partial_asyncgen():
pagen = partial(partial(agen))
assert iscoroutinefunction(pagen)

def test_gen_types():
@gen_types
def _int_types():
yield int

assert isinstance(1, (str, _int_types))
assert isinstance(5, _int_types)
assert isinstance(5.0, _int_types) is False

assert issubclass(int, (str, _int_types))
assert issubclass(int, _int_types)
assert issubclass(float, _int_types) is False

assert next(iter(_int_types())) is int
assert next(iter(_int_types)) is int
assert isinstance(_int_types, Iterable)