From ccf03b4de91edb43945b2f2c96d1e7394ee12cd8 Mon Sep 17 00:00:00 2001 From: Piotr Mardziel Date: Wed, 3 Apr 2024 16:54:14 -0700 Subject: [PATCH] remove pkg_resources and distutils (#1052) * removed pkg_resources * add reqs * remove duplicate * preserve note from duplicate * format * fix for py3.8 * format * nit * remove distutils as well and add notes * notes * nits * fix static_resource for py38 again --- format.sh | 5 +- trulens_eval/.gitignore | 1 + trulens_eval/setup.py | 44 +++- .../unit/test_feedback_score_generation.py | 1 - trulens_eval/trulens_eval/Leaderboard.py | 2 - trulens_eval/trulens_eval/__init__.py | 1 - .../trulens_eval/database/sqlalchemy_db.py | 5 +- .../trulens_eval/feedback/feedback.py | 15 +- .../trulens_eval/feedback/groundedness.py | 7 +- .../trulens_eval/feedback/provider/base.py | 1 - trulens_eval/trulens_eval/pages/Apps.py | 1 - .../trulens_eval/pages/Evaluations.py | 11 +- trulens_eval/trulens_eval/requirements.txt | 7 +- trulens_eval/trulens_eval/schema.py | 6 +- trulens_eval/trulens_eval/tru.py | 7 +- trulens_eval/trulens_eval/tru_llama.py | 2 +- trulens_eval/trulens_eval/utils/generated.py | 17 +- trulens_eval/trulens_eval/utils/imports.py | 244 ++++++++++++------ trulens_eval/trulens_eval/ux/page_config.py | 25 +- 19 files changed, 256 insertions(+), 146 deletions(-) diff --git a/format.sh b/format.sh index d7db4a05e..a4bdccbfd 100755 --- a/format.sh +++ b/format.sh @@ -10,6 +10,7 @@ else exit 1 fi +echo "Sorting imports in $FORMAT_PATH" +isort $FORMAT_PATH -s .conda -s trulens_eval/.conda echo "Formatting $FORMAT_PATH" -isort $FORMAT_PATH -yapf --style .style.yapf -r -i --verbose --parallel -r -i $FORMAT_PATH -e .conda +yapf --style .style.yapf -r -i --verbose --parallel -r -i $FORMAT_PATH -e .conda -e trulens_eval/.conda diff --git a/trulens_eval/.gitignore b/trulens_eval/.gitignore index e69de29bb..1521c8b76 100644 --- a/trulens_eval/.gitignore +++ b/trulens_eval/.gitignore @@ -0,0 +1 @@ +dist diff --git a/trulens_eval/setup.py b/trulens_eval/setup.py index e1990e096..b626553d9 100644 --- a/trulens_eval/setup.py +++ b/trulens_eval/setup.py @@ -1,34 +1,50 @@ -from distutils import log -from distutils.command.build import build +""" +# _TruLens-Eval_ build script + +To build: + +```bash +python setup.py bdist_wheel +``` + +TODO: It is more standard to configure a lot of things we configure +here in a setup.cfg file instead. It is unclear whether we can do everything +with a config file though so we may need to keep this script or parts of it. +""" + import os -from pathlib import Path -from pkg_resources import parse_requirements +from pip._internal.req import parse_requirements from setuptools import find_namespace_packages from setuptools import setup +from setuptools.command.build import build +from setuptools.logging import logging required_packages = list( map( - str, - parse_requirements(Path("trulens_eval/requirements.txt").read_text()) + lambda pip_req: str(pip_req.requirement), + parse_requirements("trulens_eval/requirements.txt", session=None) ) ) optional_packages = list( map( - str, + lambda pip_req: str(pip_req.requirement), parse_requirements( - Path("trulens_eval/requirements.optional.txt").read_text() + "trulens_eval/requirements.optional.txt", session=None ) ) ) - -class javascript_build(build): - +class BuildJavascript(build): def run(self): - log.info("running npm i") + """Custom build command to run npm commands before building the package. + + This builds the record timeline component for the dashboard. + """ + + logging.info("running npm i") os.system("npm i --prefix trulens_eval/react_components/record_viewer") - log.info("running npm run build") + logging.info("running npm run build") os.system( "npm run --prefix trulens_eval/react_components/record_viewer build" ) @@ -38,7 +54,7 @@ def run(self): setup( name="trulens_eval", cmdclass={ - 'build': javascript_build, + 'build': BuildJavascript, }, include_package_data=True, # includes things specified in MANIFEST.in packages=find_namespace_packages( diff --git a/trulens_eval/tests/unit/test_feedback_score_generation.py b/trulens_eval/tests/unit/test_feedback_score_generation.py index 7f5d58646..5668b5dbc 100644 --- a/trulens_eval/tests/unit/test_feedback_score_generation.py +++ b/trulens_eval/tests/unit/test_feedback_score_generation.py @@ -3,7 +3,6 @@ pattern matching of feedback scores from LLM responses. """ - import pytest from trulens_eval.utils.generated import ParseError diff --git a/trulens_eval/trulens_eval/Leaderboard.py b/trulens_eval/trulens_eval/Leaderboard.py index c0658b75d..bacc9114b 100644 --- a/trulens_eval/trulens_eval/Leaderboard.py +++ b/trulens_eval/trulens_eval/Leaderboard.py @@ -20,8 +20,6 @@ from trulens_eval import Tru from trulens_eval.ux import styles from trulens_eval.ux.components import draw_metadata - - from trulens_eval.ux.page_config import set_page_config set_page_config(page_title="Leaderboard") diff --git a/trulens_eval/trulens_eval/__init__.py b/trulens_eval/trulens_eval/__init__.py index 90546e547..40f538dcb 100644 --- a/trulens_eval/trulens_eval/__init__.py +++ b/trulens_eval/trulens_eval/__init__.py @@ -85,7 +85,6 @@ `utils/asynchro.py` `utils/text.py` `utils/__init__.py` - """ __version_info__ = (0, 27, 0) diff --git a/trulens_eval/trulens_eval/database/sqlalchemy_db.py b/trulens_eval/trulens_eval/database/sqlalchemy_db.py index 256535ca7..38b1059d3 100644 --- a/trulens_eval/trulens_eval/database/sqlalchemy_db.py +++ b/trulens_eval/trulens_eval/database/sqlalchemy_db.py @@ -3,8 +3,9 @@ import json import logging from sqlite3 import OperationalError -from typing import (Any, ClassVar, Dict, Iterable, List, Optional, Sequence, - Tuple, Union) +from typing import ( + Any, ClassVar, Dict, Iterable, List, Optional, Sequence, Tuple, Union +) import warnings import numpy as np diff --git a/trulens_eval/trulens_eval/feedback/feedback.py b/trulens_eval/trulens_eval/feedback/feedback.py index 69e073dcc..0931040db 100644 --- a/trulens_eval/trulens_eval/feedback/feedback.py +++ b/trulens_eval/trulens_eval/feedback/feedback.py @@ -24,11 +24,12 @@ from trulens_eval.feedback.provider.base import LLMProvider from trulens_eval.feedback.provider.endpoint.base import Endpoint -from trulens_eval.schema import AppDefinition, FeedbackOnMissingParameters +from trulens_eval.schema import AppDefinition from trulens_eval.schema import Cost from trulens_eval.schema import FeedbackCall from trulens_eval.schema import FeedbackCombinations from trulens_eval.schema import FeedbackDefinition +from trulens_eval.schema import FeedbackOnMissingParameters from trulens_eval.schema import FeedbackResult from trulens_eval.schema import FeedbackResultID from trulens_eval.schema import FeedbackResultStatus @@ -64,10 +65,13 @@ AggCallable = Callable[[Iterable[float]], float] """Signature of aggregation functions.""" + class InvalidSelector(Exception): """Raised when a selector names something that is missing in a record/app.""" - def __init__(self, selector: Lens, source_data: Optional[Dict[str, Any]] = None): + def __init__( + self, selector: Lens, source_data: Optional[Dict[str, Any]] = None + ): self.selector = selector self.source_data = source_data @@ -77,6 +81,7 @@ def __str__(self): def __repr__(self): return f"InvalidSelector({self.selector})" + def rag_triad( provider: LLMProvider, question: Optional[Lens] = None, @@ -817,7 +822,7 @@ def run( except InvalidSelector as e: # Handle the cases where a selector named something that does not # exist in source data. - + if self.if_missing == FeedbackOnMissingParameters.ERROR: feedback_result.status = FeedbackResultStatus.FAILED raise e @@ -825,8 +830,8 @@ def run( if self.if_missing == FeedbackOnMissingParameters.WARN: feedback_result.status = FeedbackResultStatus.SKIPPED logger.warning( - "Feedback %s cannot run as %s does not exist in record or app.", self.name, - e.selector + "Feedback %s cannot run as %s does not exist in record or app.", + self.name, e.selector ) return feedback_result diff --git a/trulens_eval/trulens_eval/feedback/groundedness.py b/trulens_eval/trulens_eval/feedback/groundedness.py index 6603f8213..a30dc988b 100644 --- a/trulens_eval/trulens_eval/feedback/groundedness.py +++ b/trulens_eval/trulens_eval/feedback/groundedness.py @@ -1,6 +1,8 @@ import logging from typing import Dict, List, Optional, Tuple +import nltk +from nltk.tokenize import sent_tokenize import numpy as np from tqdm.auto import tqdm @@ -17,9 +19,6 @@ from trulens_eval.utils.pyschema import WithClassInfo from trulens_eval.utils.serial import SerialModel -from nltk.tokenize import sent_tokenize -import nltk - with OptionalImports(messages=REQUIREMENT_BEDROCK): from trulens_eval.feedback.provider.bedrock import Bedrock @@ -75,7 +74,7 @@ def __init__( if groundedness_provider is None: logger.warning("Provider not provided. Using OpenAI.") groundedness_provider = OpenAI() - + nltk.download('punkt') super().__init__(groundedness_provider=groundedness_provider, **kwargs) diff --git a/trulens_eval/trulens_eval/feedback/provider/base.py b/trulens_eval/trulens_eval/feedback/provider/base.py index 89a2b756a..b82565965 100644 --- a/trulens_eval/trulens_eval/feedback/provider/base.py +++ b/trulens_eval/trulens_eval/feedback/provider/base.py @@ -1244,4 +1244,3 @@ def stereotypes_with_cot_reasons(self, prompt: str, ) return self.generate_score_and_reasons(system_prompt, user_prompt) - diff --git a/trulens_eval/trulens_eval/pages/Apps.py b/trulens_eval/trulens_eval/pages/Apps.py index 85be7819b..9c32c3b94 100644 --- a/trulens_eval/trulens_eval/pages/Apps.py +++ b/trulens_eval/trulens_eval/pages/Apps.py @@ -15,7 +15,6 @@ import streamlit as st from ux.page_config import set_page_config - st.runtime.legacy_caching.clear_cache() set_page_config(page_title="App Runner") diff --git a/trulens_eval/trulens_eval/pages/Evaluations.py b/trulens_eval/trulens_eval/pages/Evaluations.py index 04d0db0a2..b7c839cdc 100644 --- a/trulens_eval/trulens_eval/pages/Evaluations.py +++ b/trulens_eval/trulens_eval/pages/Evaluations.py @@ -39,13 +39,11 @@ from trulens_eval.ux.components import write_or_json from trulens_eval.ux.styles import cellstyle_jscode - st.runtime.legacy_caching.clear_cache() set_page_config(page_title="Evaluations") st.title("Evaluations") - tru = Tru() lms = tru.db @@ -65,9 +63,7 @@ def render_component( - query: Lens, - component: ComponentView, - header: bool=True + query: Lens, component: ComponentView, header: bool = True ) -> None: """Render the accessor/path within the wrapped app of the component.""" @@ -107,12 +103,11 @@ def render_component( def render_record_metrics( - app_df: pd.DataFrame, - selected_rows: pd.DataFrame + app_df: pd.DataFrame, selected_rows: pd.DataFrame ) -> None: """Render record level metrics (e.g. total tokens, cost, latency) compared to the average when appropriate.""" - + app_specific_df = app_df[app_df["app_id"] == selected_rows["app_id"][0]] token_col, cost_col, latency_col = st.columns(3) diff --git a/trulens_eval/trulens_eval/requirements.txt b/trulens_eval/trulens_eval/requirements.txt index 78f51a040..67e6490b4 100644 --- a/trulens_eval/trulens_eval/requirements.txt +++ b/trulens_eval/trulens_eval/requirements.txt @@ -7,7 +7,11 @@ nltk >= 3.8.1 # groundedness.py requests >= 2.31.0 nest-asyncio >= 1.5.8 typing_extensions >= 4.9.0 -psutil >= 5.9.8 # tru.py +psutil >= 5.9.8 # tru.py dashboard starting/ending +pip >= 24.0 # for requirements management + +packaging >= 23.2 # for requirements, resources management +# also: resolves version conflict with langchain-core # Secrets/env management python-dotenv >= 1.0.0 @@ -20,7 +24,6 @@ merkle-json >= 1.0.0 langchain >= 0.1.14 # required for cost tracking even outside of langchain langchain-core >= 0.1.6 # required by langchain typing-inspect >= 0.8.0 # fixes bug with langchain on python < 3.9 -packaging == 23.2 # resolves version conflict with langchain-core # Models # All models are optional. diff --git a/trulens_eval/trulens_eval/schema.py b/trulens_eval/trulens_eval/schema.py index b25517574..fca9a7e13 100644 --- a/trulens_eval/trulens_eval/schema.py +++ b/trulens_eval/trulens_eval/schema.py @@ -211,7 +211,7 @@ class Record(serial.SerialModel, Hashable): model_config: ClassVar[dict] = { # for `Future[FeedbackResult]` - 'arbitrary_types_allowed' : True + 'arbitrary_types_allowed': True } record_id: RecordID @@ -529,6 +529,7 @@ class FeedbackOnMissingParameters(str, Enum): [SKIPPED][trulens_eval.schema.FeedbackResultStatus.SKIPPED]. """ + class FeedbackCall(serial.SerialModel): """Invocations of feedback function results in one of these instances. @@ -754,7 +755,8 @@ def __init__( pyschema.Method]] = None, aggregator: Optional[Union[pyschema.Function, pyschema.Method]] = None, if_exists: Optional[serial.Lens] = None, - if_missing: FeedbackOnMissingParameters = FeedbackOnMissingParameters.ERROR, + if_missing: FeedbackOnMissingParameters = FeedbackOnMissingParameters. + ERROR, selectors: Optional[Dict[str, serial.Lens]] = None, name: Optional[str] = None, higher_is_better: Optional[bool] = None, diff --git a/trulens_eval/trulens_eval/tru.py b/trulens_eval/trulens_eval/tru.py index bfe51c4ad..743d20403 100644 --- a/trulens_eval/trulens_eval/tru.py +++ b/trulens_eval/trulens_eval/tru.py @@ -21,7 +21,6 @@ import humanize import pandas -import pkg_resources from tqdm.auto import tqdm from typing_extensions import Annotated from typing_extensions import Doc @@ -30,12 +29,12 @@ from trulens_eval import schema from trulens_eval.database import sqlalchemy_db from trulens_eval.feedback import feedback -from trulens_eval.utils import imports from trulens_eval.utils import notebook_utils from trulens_eval.utils import python from trulens_eval.utils import serial from trulens_eval.utils import text from trulens_eval.utils import threading as tru_threading +from trulens_eval.utils.imports import static_resource from trulens_eval.utils.python import Future # code style exception pp = PrettyPrinter() @@ -955,9 +954,7 @@ def run_dashboard( print("Credentials file already exists. Skipping writing process.") #run leaderboard with subprocess - leaderboard_path = pkg_resources.resource_filename( - 'trulens_eval', 'Leaderboard.py' - ) + leaderboard_path = static_resource('Leaderboard.py') if Tru._dashboard_proc is not None: print("Dashboard already running at path:", Tru._dashboard_urls) diff --git a/trulens_eval/trulens_eval/tru_llama.py b/trulens_eval/trulens_eval/tru_llama.py index 2924f563f..a0f2bd84e 100644 --- a/trulens_eval/trulens_eval/tru_llama.py +++ b/trulens_eval/trulens_eval/tru_llama.py @@ -8,7 +8,6 @@ from pprint import PrettyPrinter from typing import Any, Callable, ClassVar, Dict, Optional, Union -from pkg_resources import parse_version from pydantic import Field from trulens_eval.app import App @@ -18,6 +17,7 @@ from trulens_eval.utils.imports import Dummy from trulens_eval.utils.imports import get_package_version from trulens_eval.utils.imports import OptionalImports +from trulens_eval.utils.imports import parse_version from trulens_eval.utils.imports import REQUIREMENT_LLAMA from trulens_eval.utils.pyschema import Class from trulens_eval.utils.pyschema import FunctionOrMethod diff --git a/trulens_eval/trulens_eval/utils/generated.py b/trulens_eval/trulens_eval/utils/generated.py index 864c8b714..36ca2b9c2 100644 --- a/trulens_eval/trulens_eval/utils/generated.py +++ b/trulens_eval/trulens_eval/utils/generated.py @@ -10,19 +10,22 @@ logger = logging.getLogger(__name__) + class ParseError(Exception): """Error parsing LLM-generated text.""" - def __init__(self, expected: str, text: str, pattern: Optional[re.Pattern] = None): + def __init__( + self, expected: str, text: str, pattern: Optional[re.Pattern] = None + ): super().__init__( f"Tried to find {expected}" + - (f" using pattern {pattern.pattern}" if pattern else "") + - " in\n" + + (f" using pattern {pattern.pattern}" if pattern else "") + " in\n" + retab(tab=' ', s=text) ) self.text = text self.pattern = pattern + def validate_rating(rating) -> int: """Validate a rating is between 0 and 10.""" @@ -31,6 +34,7 @@ def validate_rating(rating) -> int: return rating + # Various old patterns that didn't work as well: # PATTERN_0_10: re.Pattern = re.compile(r"\s*([0-9]+)\s*$") # PATTERN_0_10: re.Pattern = re.compile(r"\b([0-9]|10)(?=\D*$|\s*\.)") @@ -43,6 +47,7 @@ def validate_rating(rating) -> int: PATTERN_INTEGER: re.Pattern = re.compile(r"([+-]?[1-9][0-9]*|0)") """Regex that matches integers.""" + def re_0_10_rating(s: str) -> int: """Extract a 0-10 rating from a string. @@ -75,7 +80,9 @@ def re_0_10_rating(s: str) -> int: raise ParseError("0-10 rating", s) if len(vals) > 1: - logger.warning("Multiple valid rating values found in the string: %s", s) + logger.warning( + "Multiple valid rating values found in the string: %s", s + ) # Min to handle cases like "The rating is 8 out of 10." - return min(vals) \ No newline at end of file + return min(vals) diff --git a/trulens_eval/trulens_eval/utils/imports.py b/trulens_eval/trulens_eval/utils/imports.py index 3fce24845..dc47d2028 100644 --- a/trulens_eval/trulens_eval/utils/imports.py +++ b/trulens_eval/trulens_eval/utils/imports.py @@ -6,17 +6,19 @@ import builtins from dataclasses import dataclass +from importlib import metadata +from importlib import resources + import inspect import logging from pathlib import Path from pprint import PrettyPrinter +import sys from typing import Any, Dict, Optional, Sequence, Type, Union -import pkg_resources -from pkg_resources import DistributionNotFound -from pkg_resources import get_distribution -from pkg_resources import parse_version -from pkg_resources import VersionConflict +from packaging import requirements +from packaging import version +from pip._internal.req import parse_requirements from trulens_eval import __name__ as trulens_name from trulens_eval.utils.text import retab @@ -25,81 +27,134 @@ pp = PrettyPrinter() -def requirements_of_file(path: Path) -> Dict[str, pkg_resources.Requirement]: - reqs = pkg_resources.parse_requirements(path.read_text()) - mapping = {req.project_name: req for req in reqs} +def requirements_of_file(path: Path) -> Dict[str, requirements.Requirement]: + """Get a dictionary of package names to requirements from a requirements + file.""" - return mapping + pip_reqs = parse_requirements(str(path), session=None) + mapping = {} -required_packages = requirements_of_file( - Path(pkg_resources.resource_filename("trulens_eval", "requirements.txt")) -) -optional_packages = requirements_of_file( - Path( - pkg_resources.resource_filename( - "trulens_eval", "requirements.optional.txt" - ) - ) -) + for pip_req in pip_reqs: + req = requirements.Requirement(pip_req.requirement) + mapping[req.name] = req + + return mapping -all_packages = {**required_packages, **optional_packages} +if sys.version_info >= (3, 9): + # This does not exist in 3.8 . + from importlib.abc import Traversable + _trulens_eval_resources: Traversable = resources.files("trulens_eval") + """Traversable for resources in the trulens_eval package.""" -def get_package_version(name: str): # cannot find return type - """Get the version of a package by its name. + +def static_resource(filepath: Union[Path, str]) -> Path: + """Get the path to a static resource file in the trulens_eval package. - Returns None if not installed. + By static here we mean something that exists in the filesystem already and + not in some temporary folder. We use the `importlib.resources` context + managers to get this but if the resource is temporary, the result might not + exist by the time we return or is not expected to survive long. """ - try: - return parse_version(get_distribution(name).version) + if not isinstance(filepath, Path): + filepath = Path(filepath) - except DistributionNotFound: - return None + if sys.version_info >= (3, 9): + # This does not exist in 3.8 + with resources.as_file(_trulens_eval_resources / filepath) as _path: + return _path + else: + # This is deprecated starting 3.11 + parts = filepath.parts + with resources.path("trulens_eval", parts[0]) as _path: + # NOTE: resources.path does not allow the resource to incude folders. + for part in parts[1:]: + _path = _path / part + return _path +required_packages: Dict[str, requirements.Requirement] = \ + requirements_of_file(static_resource("requirements.txt")) +"""Mapping of required package names to the requirement object with info +about that requirement including version constraints.""" -def check_imports(ignore_version_mismatch: bool = False): - """Check required and optional package versions. +optional_packages: Dict[str, requirements.Requirement] = \ + requirements_of_file(static_resource("requirements.optional.txt")) +"""Mapping of optional package names to the requirement object with info +about that requirement including version constraints.""" - Args: - ignore_version_mismatch: If set, will not raise an error if a - version mismatch is found in a required package. Regardless of - this setting, mismatch in an optional package is a warning. +all_packages: Dict[str, requirements.Requirement] = { + **required_packages, + **optional_packages +} +"""Mapping of optional and required package names to the requirement object +with info about that requirement including version constraints.""" + + +def parse_version(version_string: str) -> version.Version: + """Parse the version string into a packaging version object.""" + + return version.parse(version_string) + + +def get_package_version(name: str) -> Optional[version.Version]: + """Get the version of a package by its name. + + Returns None if given package is not installed. """ - for n, req in all_packages.items(): - is_optional = n in optional_packages + try: + return parse_version(metadata.version(name)) - try: - get_distribution(req) + except metadata.PackageNotFoundError: + return None - except VersionConflict as e: - message = f"Package {req.project_name} is installed but has a version conflict:\n\t{e}\n" +MESSAGE_DEBUG_OPTIONAL_PACKAGE_NOT_FOUND = \ +"""Optional package %s is not installed. Related optional functionality will not +be available. +""" - if is_optional: - message += f""" -This package is optional for trulens_eval so this may not be a problem but if -you need to use the related optional features and find there are errors, you -will need to resolve the conflict: +MESSAGE_ERROR_REQUIRED_PACKAGE_NOT_FOUND = \ +"""Required package {req.name} is not installed. Please install it with pip: ```bash pip install '{req}' ``` + +If your distribution is in a bad place beyond this package, you may need to +reinstall trulens_eval so that all of the dependencies get installed: + + ```bash + pip uninstall -y trulens_eval + pip install trulens_eval + ``` """ - else: - message += f""" -This package is required for trulens_eval. Please resolve the conflict by +MESSAGE_FRAGMENT_VERSION_MISMATCH = \ +"""Package {req.name} is installed but has a version conflict: + Requirement: {req} + Installed: {dist.version} +""" + +MESSAGE_FRAGMENT_VERSION_MISMATCH_OPTIONAL = \ +"""This package is optional for trulens_eval so this may not be a problem but if +you need to use the related optional features and find there are errors, you +will need to resolve the conflict: +""" + +MESSAGE_FRAGMENT_VERSION_MISMATCH_REQUIRED = \ +"""This package is required for trulens_eval. Please resolve the conflict by installing a compatible version with: +""" +MESSAGE_FRAGMENT_VERSION_MISMATCH_PIP = \ +""" ```bash pip install '{req}' ``` -""" - message += """ If you are running trulens_eval in a notebook, you may need to restart the kernel after resolving the conflict. If your distribution is in a bad place beyond this package, you may need to reinstall trulens_eval so that all of the @@ -111,41 +166,63 @@ def check_imports(ignore_version_mismatch: bool = False): ``` """ - if (not is_optional) and (not ignore_version_mismatch): - raise VersionConflict(message) from e + +class VersionConflict(Exception): + """Exception to raise when a version conflict is found in a required package.""" + + +def check_imports(ignore_version_mismatch: bool = False): + """Check required and optional package versions. + + Args: + ignore_version_mismatch: If set, will not raise an error if a + version mismatch is found in a required package. Regardless of + this setting, mismatch in an optional package is a warning. + + Raises: + VersionConflict: If a version mismatch is found in a required package + and `ignore_version_mismatch` is not set. + """ + + for n, req in all_packages.items(): + is_optional = n in optional_packages + + try: + dist = metadata.distribution(req.name) + + except metadata.PackageNotFoundError as e: + if is_optional: + logger.debug(MESSAGE_DEBUG_OPTIONAL_PACKAGE_NOT_FOUND, req.name) + else: - logger.debug(message) + raise ModuleNotFoundError( + MESSAGE_ERROR_REQUIRED_PACKAGE_NOT_FOUND.format(req=req) + ) from e + + if dist.version not in req.specifier: + message = MESSAGE_FRAGMENT_VERSION_MISMATCH.format( + req=req, dist=dist + ) - except DistributionNotFound as e: if is_optional: - logger.debug( - """ -Optional package %s is not installed. Related optional functionality will not be -available. -""", req.project_name + message += MESSAGE_FRAGMENT_VERSION_MISMATCH_OPTIONAL.format( + req=req ) else: - raise ModuleNotFoundError( - f""" -Required package {req.project_name} is not installed. Please install it with pip: + message += MESSAGE_FRAGMENT_VERSION_MISMATCH_REQUIRED.format( + req=req + ) - ```bash - pip install '{req}' - ``` + message += MESSAGE_FRAGMENT_VERSION_MISMATCH_PIP.format(req=req) -If your distribution is in a bad place beyond this package, you may need to -reinstall trulens_eval so that all of the dependencies get installed: - - ```bash - pip uninstall -y trulens_eval - pip install trulens_eval - ``` -""" - ) from e + if (not is_optional) and (not ignore_version_mismatch): + raise VersionConflict(message) + + logger.debug(message) -def pin_spec(r: pkg_resources.Requirement) -> pkg_resources.Requirement: +def pin_spec(r: requirements.Requirement) -> requirements.Requirement: """ Pin the requirement to the version assuming it is lower bounded by a version. @@ -156,13 +233,20 @@ def pin_spec(r: pkg_resources.Requirement) -> pkg_resources.Requirement: raise ValueError(f"Requirement {spec} is not lower-bounded.") spec = spec.replace(">=", "==") - return pkg_resources.Requirement.parse(spec) + return requirements.Requirement(spec) @dataclass class ImportErrorMessages(): + """Container for messages to show when an optional package is not found or + has some other import error.""" + module_not_found: str + """Message to show or raise when a package is not found.""" + import_error: str + """Message to show or raise when a package may be installed but some import + error occurred trying to import it or something from it.""" def format_import_errors( @@ -170,10 +254,12 @@ def format_import_errors( purpose: Optional[str] = None, throw: Union[bool, Exception] = False ) -> ImportErrorMessages: - """ - Format two messages for missing optional package or bad import from an optional package. Throws - an `ImportError` with the formatted message if `throw` flag is set. If `throw` is already an - exception, throws that instead after printing the message. + """Format two messages for missing optional package or bad import from an + optional package. + + Throws an `ImportError` with the formatted message if `throw` flag is set. + If `throw` is already an exception, throws that instead after printing the + message. """ if purpose is None: diff --git a/trulens_eval/trulens_eval/ux/page_config.py b/trulens_eval/trulens_eval/ux/page_config.py index 03455630f..14d6e8fcf 100644 --- a/trulens_eval/trulens_eval/ux/page_config.py +++ b/trulens_eval/trulens_eval/ux/page_config.py @@ -1,20 +1,21 @@ import base64 -import pkg_resources import streamlit as st from trulens_eval import __package__ from trulens_eval import __version__ +from trulens_eval.utils.imports import static_resource def set_page_config(page_title="TruLens"): - - st.set_page_config(page_title=page_title, page_icon="https://www.trulens.org/img/favicon.ico", layout="wide") - - logo = open( - pkg_resources.resource_filename('trulens_eval', 'ux/trulens_logo.svg'), - "rb" - ).read() + + st.set_page_config( + page_title=page_title, + page_icon="https://www.trulens.org/img/favicon.ico", + layout="wide" + ) + + logo = static_resource("ux/trulens_logo.svg").open("rb").read() logo_encoded = base64.b64encode(logo).decode() st.markdown( @@ -67,6 +68,8 @@ def set_page_config(page_title="TruLens"): with version_col: st.text(f"{__package__}\nv{__version__}") with user_feedback_col: - st.link_button("Share Feedback", "https://forms.gle/HAc4HBk5nZRpgw7C6", help="Help us improve TruLens!") - - + st.link_button( + "Share Feedback", + "https://forms.gle/HAc4HBk5nZRpgw7C6", + help="Help us improve TruLens!" + )