Skip to content

Commit

Permalink
refactor: Don’t directly import numpy and add gen_types decorator (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro authored Nov 12, 2024
1 parent 76c7981 commit 71e8e47
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 42 deletions.
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)

0 comments on commit 71e8e47

Please sign in to comment.