diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0432a655..b0ba5a98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,8 +45,14 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Upgrade pip + run: python -m pip install --upgrade pip==23.2 - name: Install LIT package with testing dependencies run: python -m pip install -e '.[test]' + - name: Debug dependency tree + run: | + python -m pip install pipdeptree + pipdeptree | grep decorator -A 5 || true - name: Test LIT run: pytest -v - name: Setup Node ${{ matrix.node-version }} diff --git a/lit_nlp/api/components.py b/lit_nlp/api/components.py index b1cdcff3..93de3f2e 100644 --- a/lit_nlp/api/components.py +++ b/lit_nlp/api/components.py @@ -137,7 +137,7 @@ def is_compatible(self, model: lit_model.Model, """True if the model and dataset support metric computation.""" for pred_spec in model.output_spec().values(): parent_key: Optional[str] = getattr(pred_spec, 'parent', None) - parent_spec: Optional[types.LitType] = dataset.spec().get(parent_key) + parent_spec: Optional[types.LitType] = dataset.spec().get(parent_key) # pyrefly: ignore[bad-argument-type] if self.is_field_compatible(pred_spec, parent_spec): return True return False diff --git a/lit_nlp/api/dataset.py b/lit_nlp/api/dataset.py index eb315891..faba9d87 100644 --- a/lit_nlp/api/dataset.py +++ b/lit_nlp/api/dataset.py @@ -127,7 +127,7 @@ def description(self) -> str: Returns: (string) A human-readable description for display in the UI. """ - return self._description or inspect.getdoc(self) or '' # pytype: disable=bad-return-type + return self._description or inspect.getdoc(self) or '' @classmethod def init_spec(cls) -> Optional[lit_types.Spec]: @@ -236,7 +236,7 @@ def remap(self, field_map: Mapping[str, str]): """Return a copy of this dataset with some fields renamed.""" new_spec = utils.remap_dict(self.spec(), field_map) new_examples = [utils.remap_dict(ex, field_map) for ex in self.examples] - return Dataset(new_spec, new_examples, base=self) + return Dataset(new_spec, new_examples, base=self) # pyrefly: ignore[bad-argument-type] @staticmethod def lit_example_from_bytes(input_bytes: bytes) -> Optional[JsonDict]: @@ -273,7 +273,7 @@ def index_inputs( indexed.append( IndexedInput( data=types.MappingProxyType( - example | {INPUT_ID_FIELD: ex_id, INPUT_META_FIELD: ex_meta} + example | {INPUT_ID_FIELD: ex_id, INPUT_META_FIELD: ex_meta} # pyrefly: ignore[unsupported-operation] ), id=ex_id, meta=ex_meta, @@ -376,6 +376,7 @@ def load(self, path: str): new_dataset = base_dataset.load(path) if base_dataset else None if new_dataset is not None: + # pyrefly: ignore[missing-attribute] description = (f'{len(new_dataset)} examples from ' f'{path}\n{self._base.description()}') return IndexedDataset( @@ -438,7 +439,7 @@ def load_lit_format( **kw, ) else: - return Dataset(spec=spec, examples=examples, *args, **kw) + return Dataset(spec=spec, examples=examples, *args, **kw) # pyrefly: ignore[bad-argument-type] # TODO(b/202210900): Remove "NoneDataset" once the LIT front-end constructs its diff --git a/lit_nlp/api/layout.py b/lit_nlp/api/layout.py index b39a2eb7..db56ae9e 100644 --- a/lit_nlp/api/layout.py +++ b/lit_nlp/api/layout.py @@ -70,7 +70,7 @@ def __call__(self, **kw): # LINT.IfChange # TODO(lit-dev): consider making modules subclass this instead of LitModuleName. @attr.s(auto_attribs=True) -class ModuleConfig(dtypes.DataTuple): +class ModuleConfig(dtypes.DataTuple): # pyrefly: ignore[invalid-inheritance] module: Union[str, LitModuleName] requiredForTab: bool = False # TODO(b/172979677): support title, duplicateAsRow, numCols, @@ -88,7 +88,7 @@ class ModuleConfig(dtypes.DataTuple): @attr.s(auto_attribs=True) -class LayoutSettings(dtypes.DataTuple): +class LayoutSettings(dtypes.DataTuple): # pyrefly: ignore[invalid-inheritance] hideToolbar: bool = False mainHeight: int = 45 leftWidth: int = 50 @@ -96,7 +96,7 @@ class LayoutSettings(dtypes.DataTuple): @attr.s(auto_attribs=True) -class LitCanonicalLayout(dtypes.DataTuple): +class LitCanonicalLayout(dtypes.DataTuple): # pyrefly: ignore[invalid-inheritance] """Frontend UI layout; should match client/lib/types.ts.""" upper: LitTabGroupLayout lower: LitTabGroupLayout = attr.ib(factory=dict) diff --git a/lit_nlp/api/model.py b/lit_nlp/api/model.py index 79558fce..32492b1e 100644 --- a/lit_nlp/api/model.py +++ b/lit_nlp/api/model.py @@ -178,12 +178,12 @@ def load(self, path: str): @abc.abstractmethod def input_spec(self) -> types.Spec: """Return a spec describing model inputs.""" - return + return # pyrefly: ignore[bad-return] @abc.abstractmethod def output_spec(self) -> types.Spec: """Return a spec describing model outputs.""" - return + return # pyrefly: ignore[bad-return] def get_embedding_table(self) -> tuple[list[str], np.ndarray]: """Return the full vocabulary and embedding table. @@ -369,7 +369,7 @@ def predict_minibatch(self, inputs: list[JsonDict]) -> list[JsonDict]: Returns: list of outputs, following model.output_spec() """ - return + return # pyrefly: ignore[bad-return] class ProjectorModel(BatchedModel, metaclass=abc.ABCMeta): diff --git a/lit_nlp/api/types.py b/lit_nlp/api/types.py index 3e7dbfd7..3851e0d3 100644 --- a/lit_nlp/api/types.py +++ b/lit_nlp/api/types.py @@ -155,10 +155,10 @@ def from_json(d: JsonDict): base_cls = globals().get("LitType") cls = globals().get(type_name) # class by name from this module - if cls is None or not issubclass(cls, base_cls): + if cls is None or not issubclass(cls, base_cls): # pyrefly: ignore[bad-argument-type] raise NameError(f"{type_name} is not a valid LitType.") - return cls(**{k: d[k] for k in d if k != "__name__"}) + return cls(**{k: d[k] for k in d if k != "__name__"}) # pyrefly: ignore[not-callable] Spec = dict[str, LitType] @@ -174,7 +174,7 @@ def _remap_leaf(leaf: LitType, keymap: dict[str, str]) -> LitType: k: (keymap.get(v, v) if k in FIELD_REF_ATTRIBUTES else v) for k, v in d.items() } - return leaf.__class__(**d) + return leaf.__class__(**d) # pyrefly: ignore[bad-argument-type] def remap_spec(spec: Spec, keymap: dict[str, str]) -> Spec: @@ -201,7 +201,7 @@ class StringLitType(LitType): Mainly used for string inputs that have special formatting, and should only be edited manually. """ - default: str = "" + default: str = "" # pyrefly: ignore[bad-override] def validate_input(self, value, spec: Spec, example: Input): if not isinstance(value, str): @@ -260,13 +260,13 @@ def validate_output(self, value, output_spec: Spec, output_dict: JsonDict, @attr.s(auto_attribs=True, frozen=True, kw_only=True) class ListLitType(LitType): """List type.""" - default: Sequence[Any] = None + default: Sequence[Any] = None # pyrefly: ignore[bad-assignment, bad-override] @attr.s(auto_attribs=True, frozen=True, kw_only=True) class _StringCandidateList(ListLitType): """A list of (text, score) tuples.""" - default: ScoredTextCandidates = None + default: ScoredTextCandidates = None # pyrefly: ignore[bad-assignment] def validate_output(self, value, output_spec: Spec, output_dict: JsonDict, input_spec: Spec, dataset_spec: Spec, @@ -373,9 +373,9 @@ class TokenTopKPreds(ListLitType): The inner list should contain (word, probability) in descending order. """ - default: Sequence[ScoredTextCandidates] = None + default: Sequence[ScoredTextCandidates] = None # pyrefly: ignore[bad-assignment] - align: str = None # name of a Tokens field in the model output + align: str = None # name of a Tokens field in the model output # pyrefly: ignore[bad-assignment] parent: Optional[str] = None def _validate_scored_candidates(self, scored_candidates): @@ -389,7 +389,7 @@ def _validate_scored_candidates(self, scored_candidates): if scored_candidate[1] is not None: if not isinstance(scored_candidate[1], NumericTypes): raise ValueError(f"{scored_candidate} second element is not a num") - if prev_val < scored_candidate[1]: + if prev_val < scored_candidate[1]: # pyrefly: ignore[unsupported-operation] raise ValueError( "TokenTopKPreds candidates are not in descending order") else: @@ -414,7 +414,7 @@ class Scalar(LitType): """Scalar value, a single float or int.""" min_val: float = 0 max_val: float = 1 - default: float = 0 + default: float = 0 # pyrefly: ignore[bad-override] step: float = .01 def validate_input(self, value, spec: Spec, example: Input): @@ -464,7 +464,7 @@ class TokenScores(_FloatList): @attr.s(auto_attribs=True, frozen=True, kw_only=True) class ReferenceScores(ListLitType): """Score of one or more target sequences.""" - default: Sequence[float] = None + default: Sequence[float] = None # pyrefly: ignore[bad-assignment] # name of a TextSegment or ReferenceTexts field in the input parent: Optional[str] = None @@ -504,7 +504,7 @@ def validate_input(self, value, spec: Spec, example: Input): @attr.s(auto_attribs=True, frozen=True, kw_only=True) class _Tensor(LitType): """A tensor type.""" - default: Sequence[float] = None + default: Sequence[float] = None # pyrefly: ignore[bad-assignment, bad-override] def validate_input(self, value, spec: Spec, example: Input): if isinstance(value, list): @@ -585,7 +585,7 @@ class SpanLabels(ListLitType): Span labels can cover more than one token, may not cover all tokens in the sentence, and may overlap with each other. """ - default: Sequence[dtypes.SpanLabel] = None + default: Sequence[dtypes.SpanLabel] = None # pyrefly: ignore[bad-assignment] align: str # name of Tokens field parent: Optional[str] = None @@ -609,7 +609,7 @@ class EdgeLabels(ListLitType): https://github.com/nyu-mll/jiant/tree/master/probing#data-format for more details. """ - default: Sequence[dtypes.EdgeLabel] = None + default: Sequence[dtypes.EdgeLabel] = None # pyrefly: ignore[bad-assignment] align: str # name of Tokens field def validate_output(self, value, output_spec: Spec, output_dict: JsonDict, @@ -636,7 +636,7 @@ class MultiSegmentAnnotations(ListLitType): TODO(lit-dev): by default, spans are treated as bytes in this context. Make this configurable, if some spans need to refer to tokens instead. """ - default: Sequence[dtypes.AnnotationCluster] = None + default: Sequence[dtypes.AnnotationCluster] = None # pyrefly: ignore[bad-assignment] exclusive: bool = False # if true, treat as candidate list background: bool = False # if true, don't emphasize in visualization @@ -775,7 +775,7 @@ class SubwordOffsets(ListLitType): offsets[i] should be the index of the first wordpiece for input token i. """ - default: Sequence[int] = None + default: Sequence[int] = None # pyrefly: ignore[bad-assignment] align_in: str # name of field in data spec align_out: str # name of field in model output spec @@ -793,7 +793,7 @@ class SparseMultilabelPreds(_StringCandidateList): The tuples are of the label and the score. """ - default: ScoredTextCandidates = None + default: ScoredTextCandidates = None # pyrefly: ignore[bad-assignment] vocab: Optional[Sequence[str]] = None # label names parent: Optional[str] = None @@ -816,7 +816,7 @@ class SingleFieldMatcher(FieldMatcher): UI will materialize this to a dropdown-list. """ - default: str = None + default: str = None # pyrefly: ignore[bad-assignment, bad-override] @attr.s(auto_attribs=True, frozen=True, kw_only=True) @@ -826,7 +826,7 @@ class MultiFieldMatcher(FieldMatcher): UI will materialize this to multiple checkboxes. Use this when the user needs to pick more than one field in UI. """ - default: Sequence[str] = [] # default names of selected items. + default: Sequence[str] = [] # default names of selected items. # pyrefly: ignore[bad-override] select_all: bool = False # Select all by default (overriddes default). @@ -840,20 +840,20 @@ class Salience(LitType): @attr.s(auto_attribs=True, frozen=True, kw_only=True) class TokenSalience(Salience): """Metadata about a returned token salience map.""" - default: dtypes.TokenSalience = None + default: dtypes.TokenSalience = None # pyrefly: ignore[bad-assignment, bad-override] @attr.s(auto_attribs=True, frozen=True, kw_only=True) class FeatureSalience(Salience): """Metadata about a returned feature salience map.""" - default: dtypes.FeatureSalience = None + default: dtypes.FeatureSalience = None # pyrefly: ignore[bad-assignment, bad-override] @attr.s(auto_attribs=True, frozen=True, kw_only=True) class FrameSalience(Salience): """Metadata about a returned frame salience map.""" - default: dtypes.FrameSalience = None + default: dtypes.FrameSalience = None # pyrefly: ignore[bad-assignment, bad-override] @attr.s(auto_attribs=True, frozen=True, kw_only=True) @@ -869,13 +869,13 @@ class ImageSalience(Salience): @attr.s(auto_attribs=True, frozen=True, kw_only=True) class SequenceSalience(Salience): """Metadata about a returned sequence salience map.""" - default: dtypes.SequenceSalienceMap = None + default: dtypes.SequenceSalienceMap = None # pyrefly: ignore[bad-assignment, bad-override] @attr.s(auto_attribs=True, frozen=True, kw_only=True) class BooleanLitType(LitType): """Boolean value.""" - default: bool = False + default: bool = False # pyrefly: ignore[bad-override] def validate_input(self, value, spec, example: Input): if not isinstance(value, bool): @@ -916,7 +916,7 @@ class MetricBestValue(dtypes.EnumSerializableAsValues, enum.Enum): @attr.s(auto_attribs=True, frozen=True, kw_only=True) class MetricResult(LitType): """Score returned from the computation of a Metric.""" - default: float = 0 + default: float = 0 # pyrefly: ignore[bad-override] description: str = "" best_value: MetricBestValue = MetricBestValue.NONE @@ -934,7 +934,7 @@ class SalienceTargetInfo(LitType): Value is a dict with keys 'field' (str) and 'index' (Optional[int]). """ - default: Optional[Mapping[str, Any]] = None + default: Optional[Mapping[str, Any]] = None # pyrefly: ignore[bad-override] # LINT.ThenChange(../client/lib/lit_types.ts) diff --git a/lit_nlp/app.py b/lit_nlp/app.py index 7d8afba5..7b6923f0 100644 --- a/lit_nlp/app.py +++ b/lit_nlp/app.py @@ -111,7 +111,7 @@ def _build_metadata(self): } # List compatible datasets. - info['datasets'] = [ + info['datasets'] = [ # pyrefly: ignore[bad-assignment] name for name, dataset in self._datasets.items() if model.is_compatible_with_dataset(dataset) ] @@ -134,13 +134,13 @@ def _build_metadata(self): _get_compatible_names(self._metrics, model, dataset) ) - info['generators'] = [ + info['generators'] = [ # pyrefly: ignore[bad-assignment] name for name in self._generators.keys() if name in compat_gens ] - info['interpreters'] = [ + info['interpreters'] = [ # pyrefly: ignore[bad-assignment] name for name in self._interpreters.keys() if name in compat_interps ] - info['metrics'] = [ + info['metrics'] = [ # pyrefly: ignore[bad-assignment] name for name in self._metrics.keys() if name in compat_metrics ] model_info[name] = info @@ -213,7 +213,7 @@ def _reconstitute_inputs( len(inputs), dataset_name, ) - return [index[ex] if isinstance(ex, str) else ex for ex in inputs] + return [index[ex] if isinstance(ex, str) else ex for ex in inputs] # pyrefly: ignore[bad-index] def _save_datapoints( self, @@ -297,16 +297,16 @@ def _get_preds(self, # Figure out what to return to the frontend. output_spec = self._get_model_spec(model)['output'] - requested_types = requested_types.split(',') if requested_types else [] - requested_fields = requested_fields.split(',') if requested_fields else [] + requested_types = requested_types.split(',') if requested_types else [] # pyrefly: ignore[bad-assignment] + requested_fields = requested_fields.split(',') if requested_fields else [] # pyrefly: ignore[bad-assignment] logging.info('Requested types: %s, fields: %s', str(requested_types), str(requested_fields)) - for t_name in requested_types: + for t_name in requested_types: # pyrefly: ignore[not-iterable] t_class = getattr(types, t_name, None) - if not issubclass(t_class, types.LitType): + if not issubclass(t_class, types.LitType): # pyrefly: ignore[bad-argument-type] raise TypeError(f"Class '{t_name}' is not a valid LitType.") - requested_fields.extend(utils.find_spec_keys(output_spec, t_class)) - ret_keys = set(requested_fields) # de-dupe + requested_fields.extend(utils.find_spec_keys(output_spec, t_class)) # pyrefly: ignore[missing-attribute] + ret_keys = set(requested_fields) # de-dupe # pyrefly: ignore[bad-argument-type] # Return selected keys. logging.info('Will return keys: %s', str(ret_keys)) @@ -336,7 +336,7 @@ def _annotate_new_data(self, example['data'] = dict(annotated_dataset.examples[i], _id=new_id) example['id'] = new_id - return data['inputs'] # pytype: disable=bad-return-type # always-use-return-annotations + return data['inputs'] def _post_new_data( self, @@ -418,6 +418,10 @@ def _create_dataset( **unused_kw, ): """Create a dataset, updating and returning the metadata.""" + if self._demo_mode: + logging.warning('Attempted to create a dataset in demo mode.') + return None + if dataset_name is None: raise ValueError('No base dataset specified.') @@ -484,7 +488,7 @@ def _create_model( Returns: A tuple containing the updated LitApp metadata and the name of the models - that were added. + that were added, or None if in demo mode. Raises: ValueError: If any of the following are missing: model_name, the config, @@ -492,6 +496,10 @@ def _create_model( configured for the provided model_name; or if there is a name collision with one of the models returned by a multiple-model loader. """ + if self._demo_mode: + logging.warning('Attempted to create a model in demo mode.') + return None + if model_name is None: raise ValueError('No base model specified.') @@ -587,11 +595,12 @@ def _get_generated( dataset = self._datasets[dataset_name] # Nested list, containing generated examples from each input. - all_generated: list[list[Input]] = genny.run( # pytype: disable=annotation-type-mismatch # always-use-return-annotations + all_generated: list[list[Input]] = genny.run( [ex['data'] for ex in data['inputs']], self._models[model], dataset, - config=config) + config=config, + ) # Annotate datapoints def annotate_generated(datapoints): @@ -861,7 +870,7 @@ def _run_annotators(self, for annotator in self._annotators: annotator.annotate(datapoints, dataset, annotated_spec) return lit_dataset.Dataset( - base=dataset, examples=datapoints, spec=annotated_spec) + base=dataset, examples=datapoints, spec=annotated_spec) # pyrefly: ignore[bad-argument-type] def make_handler(self, fn): """Convenience wrapper to handle args and serialization. @@ -1009,9 +1018,9 @@ def __init__( # Interpreter initialization if interpreters is not None: - self._interpreters = core.required_interpreters() | interpreters + self._interpreters = core.required_interpreters() | interpreters # pyrefly: ignore[unsupported-operation] else: - self._interpreters = core.default_interpreters(self._models) + self._interpreters = core.default_interpreters(self._models) # pyrefly: ignore[bad-argument-type] if metrics is not None: self._metrics = metrics diff --git a/lit_nlp/app_test.py b/lit_nlp/app_test.py new file mode 100644 index 00000000..4ef3e6a8 --- /dev/null +++ b/lit_nlp/app_test.py @@ -0,0 +1,93 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +from unittest import mock + +from absl.testing import absltest +from lit_nlp import app as lit_app +from lit_nlp.api import dataset as lit_dataset +from lit_nlp.api import types as lit_types +from lit_nlp.lib import testing_utils + + +class _TestDataset(lit_dataset.Dataset): + + def __init__(self, val: float = 1.0): + self._examples = [{'val': val}] + + def spec(self) -> lit_types.Spec: + return {'val': lit_types.Scalar()} + + def init_spec(self) -> lit_types.Spec: + return {'val': lit_types.Scalar(required=False)} + + +class AppTest(absltest.TestCase): + + def _make_app(self, demo_mode: bool) -> lit_app.LitApp: + test_model = testing_utils.IdentityRegressionModelForTesting() + return lit_app.LitApp( + models={'test_model': test_model}, + datasets={'test_ds': _TestDataset()}, + generators={}, + interpreters={}, + metrics={}, + client_root=self.create_tempdir().full_path, + demo_mode=demo_mode, + ) + + def test_create_dataset_blocked_in_demo_mode(self): + app = self._make_app(demo_mode=True) + result = app._create_dataset( + data={'config': {'new_name': 'new_ds', 'val': 2.0}}, + dataset_name='test_ds', + ) + self.assertIsNone(result) + self.assertNotIn('new_ds', app._datasets) + + def test_create_dataset_succeeds_when_not_demo_mode(self): + app = self._make_app(demo_mode=False) + result = app._create_dataset( + data={'config': {'new_name': 'new_ds', 'val': 2.0}}, + dataset_name='test_ds', + ) + self.assertIsNotNone(result) + _, new_name = result + self.assertEqual(new_name, 'new_ds') + self.assertIn('new_ds', app._datasets) + + def test_create_model_blocked_in_demo_mode(self): + app = self._make_app(demo_mode=True) + result = app._create_model( + data={'config': {'new_name': 'new_model'}}, + model_name='test_model', + ) + self.assertIsNone(result) + self.assertNotIn('new_model', app._models) + + def test_create_model_succeeds_when_not_demo_mode(self): + app = self._make_app(demo_mode=False) + result = app._create_model( + data={'config': {'new_name': 'new_model'}}, + model_name='test_model', + ) + self.assertIsNotNone(result) + _, new_names = result + self.assertEqual(new_names, ['new_model']) + self.assertIn('new_model', app._models) + + +if __name__ == '__main__': + absltest.main() diff --git a/lit_nlp/client/core/slice_module.ts b/lit_nlp/client/core/slice_module.ts index 674e652a..43b454b5 100644 --- a/lit_nlp/client/core/slice_module.ts +++ b/lit_nlp/client/core/slice_module.ts @@ -160,7 +160,7 @@ export class SliceModule extends LitModule { // clang-format off return html`
- {onKeyUp(e);}}/>