diff --git a/agbenchmark/challenges b/agbenchmark/challenges index c7daa64e..e2d7b3b8 160000 --- a/agbenchmark/challenges +++ b/agbenchmark/challenges @@ -1 +1 @@ -Subproject commit c7daa64eb72e7d4598de2c748727627898ae81e0 +Subproject commit e2d7b3b8d2cd34c2ac628debbf026b994a04346d diff --git a/agbenchmark/conftest.py b/agbenchmark/conftest.py index 07731e33..251b2ff8 100644 --- a/agbenchmark/conftest.py +++ b/agbenchmark/conftest.py @@ -14,7 +14,6 @@ from agbenchmark.reports.reports import ( generate_combined_suite_report, generate_single_call_report, session_finish, - setup_dummy_dependencies, ) from agbenchmark.start_benchmark import CONFIG_PATH, get_regression_data from agbenchmark.utils.data_types import SuiteConfig @@ -23,6 +22,8 @@ GLOBAL_TIMEOUT = ( 1500 # The tests will stop after 25 minutes so we can send the reports. ) +pytest_plugins = ["agbenchmark.utils.dependencies"] + def resolve_workspace(workspace: str) -> str: if workspace.startswith("${") and workspace.endswith("}"): @@ -159,15 +160,12 @@ def pytest_runtest_makereport(item: Any, call: Any) -> None: flags = "--test" in sys.argv or "--maintain" in sys.argv or "--improve" in sys.argv if call.when == "call": - test_name = "" # if it's a same task suite, we combine the report. # but not if it's a single --test if is_suite and is_suite.same_task and not flags: - test_name = is_suite.prefix generate_combined_suite_report(item, challenge_data, challenge_location) else: # single non suite test - test_name = challenge_data["name"] generate_single_call_report(item, call, challenge_data) # else: it's a same_task=false suite (tests aren't combined) if call.when == "teardown": @@ -204,16 +202,6 @@ def scores(request: Any) -> None: return request.node.cls.scores.get(test_class_name) -def pytest_generate_tests(metafunc: Any) -> None: - """This is to generate the dummy dependencies each test class""" - test_class_instance = metafunc.cls() - - if test_class_instance.setup_dependencies: - test_class = metafunc.cls - setup_dummy_dependencies(test_class_instance, test_class) - setattr(test_class, "setup_dependencies", []) - - # this is adding the dependency marker and category markers automatically from the json def pytest_collection_modifyitems(items: Any, config: Any) -> None: data = get_regression_data() @@ -222,7 +210,6 @@ def pytest_collection_modifyitems(items: Any, config: Any) -> None: # Assuming item.cls is your test class test_class_instance = item.cls() - # if it's a dummy dependency setup test, we also skip if "test_method" not in item.name: continue @@ -231,28 +218,22 @@ def pytest_collection_modifyitems(items: Any, config: Any) -> None: dependencies = test_class_instance.data.dependencies # Filter dependencies if they exist in regression data if its an improvement test - if ( - config.getoption("--improve") - or config.getoption("--category") - or test_class_instance.setup_dependencies # same_task suite - ): + if config.getoption("--improve") or config.getoption( + "--category" + ): # TODO: same task suite dependencies = [dep for dep in dependencies if not data.get(dep, None)] - if ( + if ( # TODO: separate task suite config.getoption("--test") - or ( # separate task suite - not test_class_instance.setup_dependencies - and config.getoption("--suite") - ) or config.getoption("--no_dep") or config.getoption("--maintain") ): dependencies = [] - categories = test_class_instance.data.category - # Add depends marker dynamically item.add_marker(pytest.mark.depends(on=dependencies, name=name)) + categories = test_class_instance.data.category + # Add category marker dynamically for category in categories: item.add_marker(getattr(pytest.mark, category)) diff --git a/agbenchmark/generate_test.py b/agbenchmark/generate_test.py index d38b5d17..b912d5b1 100644 --- a/agbenchmark/generate_test.py +++ b/agbenchmark/generate_test.py @@ -5,7 +5,7 @@ import sys import types from collections import deque from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Callable, Dict, Optional import pytest @@ -14,11 +14,52 @@ from agbenchmark.utils.challenge import Challenge from agbenchmark.utils.data_types import ChallengeData, SuiteConfig from agbenchmark.utils.utils import get_test_path +DATA_CATEGORY = {} + + +def setup_dummy_dependencies( + file_datum: list[dict[str, Any]], + challenge_class: Any, + challenge_data: ChallengeData, +) -> None: + """Sets up the dependencies if it's a suite. Creates tests that pass + based on the main test run.""" + + def create_test_func(test_name: str) -> Callable[[Any, dict[str, Any]], None]: + # This function will return another function + + # Define a dummy test function that does nothing + def setup_dependency_test(self: Any, scores: dict[str, Any]) -> None: + scores = self.get_dummy_scores(test_name, scores) + assert scores == 1 + + return setup_dependency_test + + for datum in file_datum: + DATA_CATEGORY[datum["name"]] = challenge_data.category[0] + test_func = create_test_func(datum["name"]) + # TODO: replace this once I figure out actual dependencies + test_func = pytest.mark.depends(on=[challenge_data.name], name=datum["name"])( + test_func + ) + test_func = pytest.mark.parametrize( + "challenge_data", + [None], + indirect=True, + )(test_func) + + # Add category markers + for category in challenge_data.category: + test_func = getattr(pytest.mark, category)(test_func) + + test_func = pytest.mark.usefixtures("scores")(test_func) + setattr(challenge_class, f"test_{datum['name']}", test_func) + def create_single_test( data: Dict[str, Any] | ChallengeData, challenge_location: str, - suite_config: Optional[SuiteConfig] = None, + file_datum: Optional[list[dict[str, Any]]] = None, ) -> None: challenge_data = None artifacts_location = None @@ -26,23 +67,22 @@ def create_single_test( challenge_data = data data = data.get_data() + DATA_CATEGORY[data["name"]] = data["category"][0] + # Define test class dynamically challenge_class = types.new_class(data["name"], (Challenge,)) clean_challenge_location = get_test_path(challenge_location) setattr(challenge_class, "CHALLENGE_LOCATION", clean_challenge_location) - # if its a parallel run suite we just give it the data - if suite_config and suite_config.same_task: + # in the case of a suite + if isinstance(challenge_data, ChallengeData): + if file_datum: # same task suite + setup_dummy_dependencies(file_datum, challenge_class, challenge_data) + artifacts_location = str(Path(challenge_location).resolve()) if "--test" in sys.argv or "--maintain" in sys.argv or "--improve" in sys.argv: artifacts_location = str(Path(challenge_location).resolve().parent.parent) - else: - setattr( - challenge_class, - "setup_dependencies", - [test_name for test_name in data["info"].keys()], - ) setattr( challenge_class, "_data_cache", @@ -83,15 +123,8 @@ def create_single_test( setattr(module, data["name"], challenge_class) -def create_single_suite_challenge( - suite_config: SuiteConfig, data: Dict[str, Any], path: Path -) -> None: - test_data = suite_config.challenge_from_test_data(data) - create_single_test( - test_data, - str(path), - suite_config=suite_config, - ) +def create_single_suite_challenge(challenge_data: ChallengeData, path: Path) -> None: + create_single_test(challenge_data, str(path)) def create_challenge( @@ -106,7 +139,8 @@ def create_challenge( # if its a single test running we dont care about the suite if "--test" in sys.argv or "--maintain" in sys.argv or "--improve" in sys.argv: - create_single_suite_challenge(suite_config, data, path) + challenge_data = suite_config.challenge_from_test_data(data) + create_single_suite_challenge(challenge_data, path) return json_files # Get all data.json files within the grandparent directory @@ -132,7 +166,7 @@ def create_challenge( challenge_data = suite_config.challenge_from_datum(file_datum) create_single_test( - challenge_data, str(grandparent_dir), suite_config=suite_config + challenge_data, str(grandparent_dir), file_datum=file_datum ) else: reverse = suite_config.reverse_order diff --git a/agbenchmark/reports/reports.py b/agbenchmark/reports/reports.py index 27d17589..22c4dbbb 100644 --- a/agbenchmark/reports/reports.py +++ b/agbenchmark/reports/reports.py @@ -2,9 +2,7 @@ import json import os import sys from pathlib import Path -from typing import Any, Callable - -import pytest +from typing import Any from agbenchmark.agent_interface import MOCK_FLAG from agbenchmark.reports.ReportManager import ReportManager @@ -194,41 +192,6 @@ def generate_single_call_report( item.info_details = info_details -def setup_dummy_dependencies(test_class_instance: Any, test_class: Any) -> None: - """Sets up the dependencies if it's a suite. Creates tests that pass - based on the main test run.""" - - def create_test_func(test_name: str) -> Callable[[Any, dict[str, Any]], None]: - # This function will return another function - - # Define a dummy test function that does nothing - def setup_dependency_test(self: Any, scores: dict[str, Any]) -> None: - scores = self.get_dummy_scores(test_name, scores) - assert scores == 1 - - return setup_dependency_test - - for test_name in test_class_instance.setup_dependencies: - setup_dependency_test = create_test_func(test_name) - # Add the dummy test function to the class that the current test is part of - # TODO: remove on=[test_class.__name__] and fix the actual dependencies problem - test_func = pytest.mark.depends(on=[test_class.__name__], name=test_name)( - setup_dependency_test - ) - # Parametrize to tell makereport to skip it - test_func = pytest.mark.parametrize( - "challenge_data", - [None], - indirect=True, - )(test_func) - # Add category markers - for category in test_class_instance.data.category: - test_func = getattr(pytest.mark, category)(test_func) - - test_func = pytest.mark.usefixtures("scores")(test_func) - setattr(test_class, f"test_{test_name}", test_func) - - def finalize_reports(item: Any, challenge_data: dict[str, Any]) -> None: run_time = dict(item.user_properties).get("run_time") diff --git a/agbenchmark/start_benchmark.py b/agbenchmark/start_benchmark.py index 56ebed14..559eecee 100644 --- a/agbenchmark/start_benchmark.py +++ b/agbenchmark/start_benchmark.py @@ -22,6 +22,7 @@ if os.environ.get("HELICONE_API_KEY"): "benchmark_start_time", BENCHMARK_START_TIME ) + ( HOME_DIRECTORY, CONFIG_PATH, diff --git a/agbenchmark/utils/challenge.py b/agbenchmark/utils/challenge.py index 8ecd013d..0831621b 100644 --- a/agbenchmark/utils/challenge.py +++ b/agbenchmark/utils/challenge.py @@ -25,7 +25,6 @@ class Challenge(ABC): _data_cache: Dict[str, ChallengeData] = {} CHALLENGE_LOCATION: str = "" ARTIFACTS_LOCATION: str = "" # this is for suites - setup_dependencies: List[str] = [] # this is for suites scores: dict[str, Any] = {} # this is for suites @property diff --git a/agbenchmark/utils/dependencies/__init__.py b/agbenchmark/utils/dependencies/__init__.py new file mode 100644 index 00000000..63ebdb5c --- /dev/null +++ b/agbenchmark/utils/dependencies/__init__.py @@ -0,0 +1,185 @@ +""" +A module that provides the pytest hooks for this plugin. + +The logic itself is in main.py. +""" + +import warnings +from typing import Any, Callable, Optional + +import pytest +from _pytest.config.argparsing import OptionGroup +from _pytest.nodes import Item + +from .main import DependencyManager + +# Each test suite run should have a single manager object. For regular runs, a simple singleton would suffice, but for +# our own tests this causes problems, as the nested pytest runs get the same instance. This can be worked around by +# running them all in subprocesses, but this slows the tests down massively. Instead, keep a stack of managers, so each +# test suite will have its own manager, even nested ones. +managers: list[DependencyManager] = [] + + +DEPENDENCY_PROBLEM_ACTIONS: dict[str, Callable[[str], None] | None] = { + "run": None, + "skip": lambda m: pytest.skip(m), + "fail": lambda m: pytest.fail(m, False), + "warning": lambda m: warnings.warn(m), +} + + +def _add_ini_and_option( + parser: Any, + group: OptionGroup, + name: str, + help: str, + default: str | bool | int, + **kwargs: Any, +) -> None: + """Add an option to both the ini file as well as the command line flags, with the latter overriding the former.""" + parser.addini( + name, + help + " This overrides the similarly named option from the config.", + default=default, + ) + group.addoption(f'--{name.replace("_", "-")}', help=help, default=None, **kwargs) + + +def _get_ini_or_option( + config: Any, name: str, choices: Optional[list[str]] +) -> str | None: + """Get an option from either the ini file or the command line flags, the latter taking precedence.""" + value = config.getini(name) + if value is not None and choices is not None and value not in choices: + raise ValueError( + f'Invalid ini value for {name}, choose from {", ".join(choices)}' + ) + return config.getoption(name) or value + + +def pytest_addoption(parser: Any) -> None: + group = parser.getgroup("depends") + + # Add a flag to list all names + the tests they resolve to + group.addoption( + "--list-dependency-names", + action="store_true", + default=False, + help=( + "List all non-nodeid dependency names + the tests they resolve to. " + "Will also list all nodeid dependency names when verbosity is high enough." + ), + ) + + # Add a flag to list all (resolved) dependencies for all tests + unresolvable names + group.addoption( + "--list-processed-dependencies", + action="store_true", + default=False, + help="List all dependencies of all tests as a list of nodeids + the names that could not be resolved.", + ) + + # Add an ini option + flag to choose the action to take for failed dependencies + _add_ini_and_option( + parser, + group, + name="failed_dependency_action", + help=( + "The action to take when a test has dependencies that failed. " + 'Use "run" to run the test anyway, "skip" to skip the test, and "fail" to fail the test.' + ), + default="skip", + choices=DEPENDENCY_PROBLEM_ACTIONS.keys(), + ) + + # Add an ini option + flag to choose the action to take for unresolved dependencies + _add_ini_and_option( + parser, + group, + name="missing_dependency_action", + help=( + "The action to take when a test has dependencies that cannot be found within the current scope. " + 'Use "run" to run the test anyway, "skip" to skip the test, and "fail" to fail the test.' + ), + default="warning", + choices=DEPENDENCY_PROBLEM_ACTIONS.keys(), + ) + + +def pytest_configure(config: Any) -> None: + manager = DependencyManager() + managers.append(manager) + + # Setup the handling of problems with dependencies + manager.options["failed_dependency_action"] = _get_ini_or_option( + config, + "failed_dependency_action", + list(DEPENDENCY_PROBLEM_ACTIONS.keys()), + ) + manager.options["missing_dependency_action"] = _get_ini_or_option( + config, + "missing_dependency_action", + list(DEPENDENCY_PROBLEM_ACTIONS.keys()), + ) + + # Register marker + config.addinivalue_line( + "markers", + "depends(name='name', on=['other_name']): marks depencies between tests.", + ) + + +@pytest.hookimpl(trylast=True) +def pytest_collection_modifyitems(config: Any, items: list[Item]) -> None: + manager = managers[-1] + + # Register the founds tests on the manager + manager.items = items + + # Show the extra information if requested + if config.getoption("list_dependency_names"): + verbose = config.getoption("verbose") > 1 + manager.print_name_map(verbose) + if config.getoption("list_processed_dependencies"): + color = config.getoption("color") + manager.print_processed_dependencies(color) + + # Reorder the items so that tests run after their dependencies + items[:] = manager.sorted_items + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item: Item) -> Any: + manager = managers[-1] + + # Run the step + outcome = yield + + # Store the result on the manager + manager.register_result(item, outcome.get_result()) + + +def pytest_runtest_call(item: Item) -> None: + manager = managers[-1] + + # Handle missing dependencies + missing_dependency_action = DEPENDENCY_PROBLEM_ACTIONS[ + manager.options["missing_dependency_action"] + ] + missing = manager.get_missing(item) + if missing_dependency_action and missing: + missing_dependency_action( + f'{item.nodeid} depends on {", ".join(missing)}, which was not found' + ) + + # Check whether all dependencies succeeded + failed_dependency_action = DEPENDENCY_PROBLEM_ACTIONS[ + manager.options["failed_dependency_action"] + ] + failed = manager.get_failed(item) + if failed_dependency_action and failed: + failed_dependency_action(f'{item.nodeid} depends on {", ".join(failed)}') + + +def pytest_unconfigure() -> None: + managers.pop() diff --git a/agbenchmark/utils/dependencies/constants.py b/agbenchmark/utils/dependencies/constants.py new file mode 100644 index 00000000..98f60a5e --- /dev/null +++ b/agbenchmark/utils/dependencies/constants.py @@ -0,0 +1,10 @@ +""" Constants for this module. """ + +# The name of the marker used +MARKER_NAME = "depends" + +# The name of the keyword argument for the marker that contains custom name(s) for the tests +MARKER_KWARG_ID = "name" + +# The name of the keyword argument for the marker that specifies the tests to depend on +MARKER_KWARG_DEPENDENCIES = "on" diff --git a/agbenchmark/utils/dependencies/graphs.py b/agbenchmark/utils/dependencies/graphs.py new file mode 100644 index 00000000..92f63c4f --- /dev/null +++ b/agbenchmark/utils/dependencies/graphs.py @@ -0,0 +1,265 @@ +import math +from pathlib import Path +from typing import Any, Dict, List, Tuple + +import matplotlib.patches as patches +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np +from pyvis.network import Network + +from agbenchmark.generate_test import DATA_CATEGORY + + +def bezier_curve( + src: np.ndarray, ctrl: List[float], dst: np.ndarray +) -> List[np.ndarray]: + """ + Generate Bézier curve points. + + Args: + - src (np.ndarray): The source point. + - ctrl (List[float]): The control point. + - dst (np.ndarray): The destination point. + + Returns: + - List[np.ndarray]: The Bézier curve points. + """ + curve = [] + for t in np.linspace(0, 1, num=100): + curve_point = ( + np.outer((1 - t) ** 2, src) + + 2 * np.outer((1 - t) * t, ctrl) + + np.outer(t**2, dst) + ) + curve.append(curve_point[0]) + return curve + + +def curved_edges( + G: nx.Graph, pos: Dict[Any, Tuple[float, float]], dist: float = 0.2 +) -> None: + """ + Draw curved edges for nodes on the same level. + + Args: + - G (Any): The graph object. + - pos (Dict[Any, Tuple[float, float]]): Dictionary with node positions. + - dist (float, optional): Distance for curvature. Defaults to 0.2. + + Returns: + - None + """ + ax = plt.gca() + for u, v, data in G.edges(data=True): + src = np.array(pos[u]) + dst = np.array(pos[v]) + + same_level = abs(src[1] - dst[1]) < 0.01 + + if same_level: + control = [(src[0] + dst[0]) / 2, src[1] + dist] + curve = bezier_curve(src, control, dst) + arrow = patches.FancyArrowPatch( + posA=curve[0], # type: ignore + posB=curve[-1], # type: ignore + connectionstyle=f"arc3,rad=0.2", + color="gray", + arrowstyle="-|>", + mutation_scale=15.0, + lw=1, + shrinkA=10, + shrinkB=10, + ) + ax.add_patch(arrow) + else: + ax.annotate( + "", + xy=dst, + xytext=src, + arrowprops=dict( + arrowstyle="-|>", color="gray", lw=1, shrinkA=10, shrinkB=10 + ), + ) + + +def tree_layout(graph: nx.DiGraph, root_node: Any) -> Dict[Any, Tuple[float, float]]: + """Compute positions as a tree layout centered on the root with alternating vertical shifts.""" + bfs_tree = nx.bfs_tree(graph, source=root_node) + levels = { + node: depth + for node, depth in nx.single_source_shortest_path_length( + bfs_tree, root_node + ).items() + } + + pos = {} + max_depth = max(levels.values()) + level_positions = {i: 0 for i in range(max_depth + 1)} # type: ignore + + # Count the number of nodes per level to compute the width + level_count: Any = {} + for node, level in levels.items(): + level_count[level] = level_count.get(level, 0) + 1 + + vertical_offset = ( + 0.07 # The amount of vertical shift per node within the same level + ) + + # Assign positions + for node, level in sorted(levels.items(), key=lambda x: x[1]): + total_nodes_in_level = level_count[level] + horizontal_spacing = 1.0 / (total_nodes_in_level + 1) + pos_x = ( + 0.5 + - (total_nodes_in_level - 1) * horizontal_spacing / 2 + + level_positions[level] * horizontal_spacing + ) + + # Alternately shift nodes up and down within the same level + pos_y = ( + -level + + (level_positions[level] % 2) * vertical_offset + - ((level_positions[level] + 1) % 2) * vertical_offset + ) + pos[node] = (pos_x, pos_y) + + level_positions[level] += 1 + + return pos + + +def graph_spring_layout( + dag: nx.DiGraph, labels: Dict[Any, str], tree: bool = True +) -> None: + num_nodes = len(dag.nodes()) + # Setting up the figure and axis + fig, ax = plt.subplots() + ax.axis("off") # Turn off the axis + + base = 3.0 + + if num_nodes > 10: + base /= 1 + math.log(num_nodes) + font_size = base * 10 + + font_size = max(10, base * 10) + node_size = max(300, base * 1000) + + if tree: + root_node = [node for node, degree in dag.in_degree() if degree == 0][0] + pos = tree_layout(dag, root_node) + else: + # Adjust k for the spring layout based on node count + k_value = 3 / math.sqrt(num_nodes) + + pos = nx.spring_layout(dag, k=k_value, iterations=50) + + # Draw nodes and labels + nx.draw_networkx_nodes(dag, pos, node_color="skyblue", node_size=int(node_size)) + nx.draw_networkx_labels(dag, pos, labels=labels, font_size=int(font_size)) + + # Draw curved edges + curved_edges(dag, pos) # type: ignore + + plt.tight_layout() + plt.show() + + +def rgb_to_hex(rgb: Tuple[float, float, float]) -> str: + return "#{:02x}{:02x}{:02x}".format( + int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255) + ) + + +def get_category_colors(categories: Dict[Any, str]) -> Dict[str, str]: + unique_categories = set(categories.values()) + colormap = plt.cm.get_cmap("tab10", len(unique_categories)) # type: ignore + return { + category: rgb_to_hex(colormap(i)[:3]) + for i, category in enumerate(unique_categories) + } + + +def graph_interactive_network( + dag: nx.DiGraph, labels: Dict[Any, str], show: bool = False +) -> None: + nt = Network(notebook=True, width="100%", height="800px", directed=True) + + category_colors = get_category_colors(DATA_CATEGORY) + + # Add nodes and edges to the pyvis network + for node, label in labels.items(): + node_id_str = node.nodeid + + # Get the category for this label + category = DATA_CATEGORY.get( + label, "unknown" + ) # Default to 'unknown' if label not found + + # Get the color for this category + color = category_colors.get(category, "grey") + + nt.add_node(node_id_str, label=label, color=color) + + # Add edges to the pyvis network + for edge in dag.edges(): + source_id_str = edge[0].nodeid + target_id_str = edge[1].nodeid + if not (source_id_str in nt.get_nodes() and target_id_str in nt.get_nodes()): + print( + f"Skipping edge {source_id_str} -> {target_id_str} due to missing nodes." + ) + continue + nt.add_edge(source_id_str, target_id_str) + + # Configure physics for hierarchical layout + hierarchical_options = { + "enabled": True, + "levelSeparation": 200, # Increased vertical spacing between levels + "nodeSpacing": 250, # Increased spacing between nodes on the same level + "treeSpacing": 250, # Increased spacing between different trees (for forest) + "blockShifting": True, + "edgeMinimization": True, + "parentCentralization": True, + "direction": "UD", + "sortMethod": "directed", + } + + physics_options = { + "stabilization": { + "enabled": True, + "iterations": 1000, # Default is often around 100 + }, + "hierarchicalRepulsion": { + "centralGravity": 0.0, + "springLength": 200, # Increased edge length + "springConstant": 0.01, + "nodeDistance": 250, # Increased minimum distance between nodes + "damping": 0.09, + }, + "solver": "hierarchicalRepulsion", + "timestep": 0.5, + } + + nt.options = { + "nodes": { + "font": { + "size": 20, # Increased font size for labels + "color": "black", # Set a readable font color + }, + "shapeProperties": {"useBorderWithImage": True}, + }, + "edges": { + "length": 250, # Increased edge length + }, + "physics": physics_options, + "layout": {"hierarchical": hierarchical_options}, + } + + relative_path = "agbenchmark/challenges/dependencies.html" + file_path = str(Path(relative_path).resolve()) + + if show: + nt.show(file_path, notebook=False) + nt.write_html(file_path) diff --git a/agbenchmark/utils/dependencies/main.py b/agbenchmark/utils/dependencies/main.py new file mode 100644 index 00000000..04c5fafe --- /dev/null +++ b/agbenchmark/utils/dependencies/main.py @@ -0,0 +1,239 @@ +""" +A module to manage dependencies between pytest tests. + +This module provides the methods implementing the main logic. These are used in the pytest hooks that are in +__init__.py. +""" + +import collections +from typing import Any, Generator + +import colorama +import networkx +from _pytest.nodes import Item + +from .constants import MARKER_KWARG_DEPENDENCIES, MARKER_NAME +from .graphs import graph_interactive_network +from .util import clean_nodeid, get_absolute_nodeid, get_markers, get_name + + +class TestResult(object): + """Keeps track of the results of a single test.""" + + STEPS = ["setup", "call", "teardown"] + GOOD_OUTCOMES = ["passed"] + + def __init__(self, nodeid: str) -> None: + """Create a new instance for a test with a given node id.""" + self.nodeid = nodeid + self.results: dict[str, Any] = {} + + def register_result(self, result: Any) -> None: + """Register a result of this test.""" + if result.when not in self.STEPS: + raise ValueError( + f"Received result for unknown step {result.when} of test {self.nodeid}" + ) + if result.when in self.results: + raise AttributeError( + f"Received multiple results for step {result.when} of test {self.nodeid}" + ) + self.results[result.when] = result.outcome + + @property + def success(self) -> bool: + """Whether the entire test was successful.""" + return all( + self.results.get(step, None) in self.GOOD_OUTCOMES for step in self.STEPS + ) + + +class TestDependencies(object): + """Information about the resolved dependencies of a single test.""" + + def __init__(self, item: Item, manager: "DependencyManager") -> None: + """Create a new instance for a given test.""" + self.nodeid = clean_nodeid(item.nodeid) + self.dependencies = set() + self.unresolved = set() + + markers = get_markers(item, MARKER_NAME) + dependencies = [ + dep + for marker in markers + for dep in marker.kwargs.get(MARKER_KWARG_DEPENDENCIES, []) + ] + for dependency in dependencies: + # If the name is not known, try to make it absolute (ie file::[class::]method) + if dependency not in manager.name_to_nodeids: + absolute_dependency = get_absolute_nodeid(dependency, self.nodeid) + if absolute_dependency in manager.name_to_nodeids: + dependency = absolute_dependency + + # Add all items matching the name + if dependency in manager.name_to_nodeids: + for nodeid in manager.name_to_nodeids[dependency]: + self.dependencies.add(nodeid) + else: + self.unresolved.add(dependency) + + +class DependencyManager(object): + """Keep track of tests, their names and their dependencies.""" + + def __init__(self) -> None: + """Create a new DependencyManager.""" + self.options: dict[str, Any] = {} + self._items: list[Item] | None = None + self._name_to_nodeids: Any = None + self._nodeid_to_item: Any = None + self._results: Any = None + + @property + def items(self) -> list[Item]: + """The collected tests that are managed by this instance.""" + if self._items is None: + raise AttributeError("The items attribute has not been set yet") + return self._items + + @items.setter + def items(self, items: list[Item]) -> None: + if self._items is not None: + raise AttributeError("The items attribute has already been set") + self._items = items + + self._name_to_nodeids = collections.defaultdict(list) + self._nodeid_to_item = {} + self._results = {} + self._dependencies = {} + + for item in items: + nodeid = clean_nodeid(item.nodeid) + # Add the mapping from nodeid to the test item + self._nodeid_to_item[nodeid] = item + # Add the mappings from all names to the node id + name = get_name(item) + self._name_to_nodeids[name].append(nodeid) + # Create the object that will contain the results of this test + self._results[nodeid] = TestResult(clean_nodeid(item.nodeid)) + + # Don't allow using unknown keys on the name_to_nodeids mapping + self._name_to_nodeids.default_factory = None + + for item in items: + nodeid = clean_nodeid(item.nodeid) + # Process the dependencies of this test + # This uses the mappings created in the previous loop, and can thus not be merged into that loop + self._dependencies[nodeid] = TestDependencies(item, self) + + @property + def name_to_nodeids(self) -> dict[str, list[str]]: + """A mapping from names to matching node id(s).""" + assert self.items is not None + return self._name_to_nodeids + + @property + def nodeid_to_item(self) -> dict[str, Item]: + """A mapping from node ids to test items.""" + assert self.items is not None + return self._nodeid_to_item + + @property + def results(self) -> dict[str, TestResult]: + """The results of the tests.""" + assert self.items is not None + return self._results + + @property + def dependencies(self) -> dict[str, TestDependencies]: + """The dependencies of the tests.""" + assert self.items is not None + return self._dependencies + + def print_name_map(self, verbose: bool = False) -> None: + """Print a human-readable version of the name -> test mapping.""" + print("Available dependency names:") + for name, nodeids in sorted(self.name_to_nodeids.items(), key=lambda x: x[0]): + if len(nodeids) == 1: + if name == nodeids[0]: + # This is just the base name, only print this when verbose + if verbose: + print(f" {name}") + else: + # Name refers to a single node id, so use the short format + print(f" {name} -> {nodeids[0]}") + else: + # Name refers to multiple node ids, so use the long format + print(f" {name} ->") + for nodeid in sorted(nodeids): + print(f" {nodeid}") + + def print_processed_dependencies(self, colors: bool = False) -> None: + """Print a human-readable list of the processed dependencies.""" + missing = "MISSING" + if colors: + missing = f"{colorama.Fore.RED}{missing}{colorama.Fore.RESET}" + colorama.init() + try: + print("Dependencies:") + for nodeid, info in sorted(self.dependencies.items(), key=lambda x: x[0]): + descriptions = [] + for dependency in info.dependencies: + descriptions.append(dependency) + for dependency in info.unresolved: + descriptions.append(f"{dependency} ({missing})") + if descriptions: + print(f" {nodeid} depends on") + for description in sorted(descriptions): + print(f" {description}") + finally: + if colors: + colorama.deinit() + + @property + def sorted_items(self, show_graph: bool = False) -> Generator: + """Get a sorted list of tests where all tests are sorted after their dependencies.""" + # Build a directed graph for sorting + dag = networkx.DiGraph() + + # Insert all items as nodes, to prevent items that have no dependencies and are not dependencies themselves from + # being lost + dag.add_nodes_from(self.items) + + # Insert edges for all the dependencies + for item in self.items: + nodeid = clean_nodeid(item.nodeid) + for dependency in self.dependencies[nodeid].dependencies: + dag.add_edge(self.nodeid_to_item[dependency], item) + + labels = {} + for item in self.items: + node_name = get_name(item) + labels[item] = node_name + + if show_graph: + # graph_spring_layout(dag, labels) + graph_interactive_network(dag, labels, show=False) + + # Sort based on the dependencies + return networkx.topological_sort(dag) + + def register_result(self, item: Item, result: Any) -> None: + """Register a result of a test.""" + nodeid = clean_nodeid(item.nodeid) + self.results[nodeid].register_result(result) + + def get_failed(self, item: Item) -> Any: + """Get a list of unfulfilled dependencies for a test.""" + nodeid = clean_nodeid(item.nodeid) + failed = [] + for dependency in self.dependencies[nodeid].dependencies: + result = self.results[dependency] + if not result.success: + failed.append(dependency) + return failed + + def get_missing(self, item: Item) -> Any: + """Get a list of missing dependencies for a test.""" + nodeid = clean_nodeid(item.nodeid) + return self.dependencies[nodeid].unresolved diff --git a/agbenchmark/utils/dependencies/util.py b/agbenchmark/utils/dependencies/util.py new file mode 100644 index 00000000..f7f4664e --- /dev/null +++ b/agbenchmark/utils/dependencies/util.py @@ -0,0 +1,85 @@ +""" Utility functions to process the identifiers of tests. """ +import re +from typing import Iterator + +from _pytest.mark.structures import Mark +from _pytest.nodes import Item + +from .constants import MARKER_KWARG_ID, MARKER_NAME + +REGEX_PARAMETERS = re.compile(r"\[.+\]$") + + +def clean_nodeid(nodeid: str) -> str: + """ + Remove any superfluous ::() from a node id. + + >>> clean_nodeid('test_file.py::TestClass::()::test') + 'test_file.py::TestClass::test' + >>> clean_nodeid('test_file.py::TestClass::test') + 'test_file.py::TestClass::test' + >>> clean_nodeid('test_file.py::test') + 'test_file.py::test' + """ + return nodeid.replace("::()::", "::") + + +def strip_nodeid_parameters(nodeid: str) -> str: + """ + Strip parameters from a node id. + + >>> strip_nodeid_parameters('test_file.py::TestClass::test[foo]') + 'test_file.py::TestClass::test' + >>> strip_nodeid_parameters('test_file.py::TestClass::test') + 'test_file.py::TestClass::test' + """ + return REGEX_PARAMETERS.sub("", nodeid) + + +def get_absolute_nodeid(nodeid: str, scope: str) -> str: + """ + Transform a possibly relative node id to an absolute one using the scope in which it is used. + + >>> scope = 'test_file.py::TestClass::test' + >>> get_absolute_nodeid('test2', scope) + 'test_file.py::TestClass::test2' + >>> get_absolute_nodeid('TestClass2::test2', scope) + 'test_file.py::TestClass2::test2' + >>> get_absolute_nodeid('test_file2.py::TestClass2::test2', scope) + 'test_file2.py::TestClass2::test2' + """ + parts = nodeid.split("::") + # Completely relative (test_name), so add the full current scope (either file::class or file) + if len(parts) == 1: + base_nodeid = scope.rsplit("::", 1)[0] + nodeid = f"{base_nodeid}::{nodeid}" + # Contains some scope already (Class::test_name), so only add the current file scope + elif "." not in parts[0]: + base_nodeid = scope.split("::", 1)[0] + nodeid = f"{base_nodeid}::{nodeid}" + return clean_nodeid(nodeid) + + +def get_name(item: Item) -> str: + """ + Get all names for a test. + + This will use the following methods to determine the name of the test: + - If given, the custom name(s) passed to the keyword argument name on the marker + """ + name = "" + + # Custom name + markers = get_markers(item, MARKER_NAME) + for marker in markers: + if MARKER_KWARG_ID in marker.kwargs: + name = marker.kwargs[MARKER_KWARG_ID] + + return name + + +def get_markers(item: Item, name: str) -> Iterator[Mark]: + """Get all markers with the given name for a given item.""" + for marker in item.iter_markers(): + if marker.name == name: + yield marker diff --git a/poetry.lock b/poetry.lock index 627416e9..8130bc65 100644 --- a/poetry.lock +++ b/poetry.lock @@ -162,6 +162,34 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + [[package]] name = "async-timeout" version = "4.0.2" @@ -206,6 +234,17 @@ files = [ pyflakes = ">=1.1.0,<3" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + [[package]] name = "black" version = "22.3.0" @@ -451,6 +490,17 @@ files = [ {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, ] +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + [[package]] name = "exceptiongroup" version = "1.1.2" @@ -465,6 +515,20 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = "*" +files = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, +] + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + [[package]] name = "fastapi" version = "0.100.1" @@ -502,45 +566,45 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "fonttools" -version = "4.41.1" +version = "4.42.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a7bbb290d13c6dd718ec2c3db46fe6c5f6811e7ea1e07f145fd8468176398224"}, - {file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec453a45778524f925a8f20fd26a3326f398bfc55d534e37bab470c5e415caa1"}, - {file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2071267deaa6d93cb16288613419679c77220543551cbe61da02c93d92df72f"}, - {file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e3334d51f0e37e2c6056e67141b2adabc92613a968797e2571ca8a03bd64773"}, - {file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cac73bbef7734e78c60949da11c4903ee5837168e58772371bd42a75872f4f82"}, - {file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:edee0900cf0eedb29d17c7876102d6e5a91ee333882b1f5abc83e85b934cadb5"}, - {file = "fonttools-4.41.1-cp310-cp310-win32.whl", hash = "sha256:2a22b2c425c698dcd5d6b0ff0b566e8e9663172118db6fd5f1941f9b8063da9b"}, - {file = "fonttools-4.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:547ab36a799dded58a46fa647266c24d0ed43a66028cd1cd4370b246ad426cac"}, - {file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:849ec722bbf7d3501a0e879e57dec1fc54919d31bff3f690af30bb87970f9784"}, - {file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38cdecd8f1fd4bf4daae7fed1b3170dfc1b523388d6664b2204b351820aa78a7"}, - {file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ae64303ba670f8959fdaaa30ba0c2dabe75364fdec1caeee596c45d51ca3425"}, - {file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14f3ccea4cc7dd1b277385adf3c3bf18f9860f87eab9c2fb650b0af16800f55"}, - {file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:33191f062549e6bb1a4782c22a04ebd37009c09360e2d6686ac5083774d06d95"}, - {file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:704bccd69b0abb6fab9f5e4d2b75896afa48b427caa2c7988792a2ffce35b441"}, - {file = "fonttools-4.41.1-cp311-cp311-win32.whl", hash = "sha256:4edc795533421e98f60acee7d28fc8d941ff5ac10f44668c9c3635ad72ae9045"}, - {file = "fonttools-4.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:aaaef294d8e411f0ecb778a0aefd11bb5884c9b8333cc1011bdaf3b58ca4bd75"}, - {file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3d1f9471134affc1e3b1b806db6e3e2ad3fa99439e332f1881a474c825101096"}, - {file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59eba8b2e749a1de85760da22333f3d17c42b66e03758855a12a2a542723c6e7"}, - {file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9b3cc10dc9e0834b6665fd63ae0c6964c6bc3d7166e9bc84772e0edd09f9fa2"}, - {file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2c2964bdc827ba6b8a91dc6de792620be4da3922c4cf0599f36a488c07e2b2"}, - {file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7763316111df7b5165529f4183a334aa24c13cdb5375ffa1dc8ce309c8bf4e5c"}, - {file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b2d1ee95be42b80d1f002d1ee0a51d7a435ea90d36f1a5ae331be9962ee5a3f1"}, - {file = "fonttools-4.41.1-cp38-cp38-win32.whl", hash = "sha256:f48602c0b3fd79cd83a34c40af565fe6db7ac9085c8823b552e6e751e3a5b8be"}, - {file = "fonttools-4.41.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0938ebbeccf7c80bb9a15e31645cf831572c3a33d5cc69abe436e7000c61b14"}, - {file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e5c2b0a95a221838991e2f0e455dec1ca3a8cc9cd54febd68cc64d40fdb83669"}, - {file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:891cfc5a83b0307688f78b9bb446f03a7a1ad981690ac8362f50518bc6153975"}, - {file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73ef0bb5d60eb02ba4d3a7d23ada32184bd86007cb2de3657cfcb1175325fc83"}, - {file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f240d9adf0583ac8fc1646afe7f4ac039022b6f8fa4f1575a2cfa53675360b69"}, - {file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bdd729744ae7ecd7f7311ad25d99da4999003dcfe43b436cf3c333d4e68de73d"}, - {file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b927e5f466d99c03e6e20961946314b81d6e3490d95865ef88061144d9f62e38"}, - {file = "fonttools-4.41.1-cp39-cp39-win32.whl", hash = "sha256:afce2aeb80be72b4da7dd114f10f04873ff512793d13ce0b19d12b2a4c44c0f0"}, - {file = "fonttools-4.41.1-cp39-cp39-win_amd64.whl", hash = "sha256:1df1b6f4c7c4bc8201eb47f3b268adbf2539943aa43c400f84556557e3e109c0"}, - {file = "fonttools-4.41.1-py3-none-any.whl", hash = "sha256:952cb405f78734cf6466252fec42e206450d1a6715746013f64df9cbd4f896fa"}, - {file = "fonttools-4.41.1.tar.gz", hash = "sha256:e16a9449f21a93909c5be2f5ed5246420f2316e94195dbfccb5238aaa38f9751"}, + {file = "fonttools-4.42.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9c456d1f23deff64ffc8b5b098718e149279abdea4d8692dba69172fb6a0d597"}, + {file = "fonttools-4.42.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:150122ed93127a26bc3670ebab7e2add1e0983d30927733aec327ebf4255b072"}, + {file = "fonttools-4.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48e82d776d2e93f88ca56567509d102266e7ab2fb707a0326f032fe657335238"}, + {file = "fonttools-4.42.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58c1165f9b2662645de9b19a8c8bdd636b36294ccc07e1b0163856b74f10bafc"}, + {file = "fonttools-4.42.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d6dc3fa91414ff4daa195c05f946e6a575bd214821e26d17ca50f74b35b0fe4"}, + {file = "fonttools-4.42.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fae4e801b774cc62cecf4a57b1eae4097903fced00c608d9e2bc8f84cd87b54a"}, + {file = "fonttools-4.42.0-cp310-cp310-win32.whl", hash = "sha256:b8600ae7dce6ec3ddfb201abb98c9d53abbf8064d7ac0c8a0d8925e722ccf2a0"}, + {file = "fonttools-4.42.0-cp310-cp310-win_amd64.whl", hash = "sha256:57b68eab183fafac7cd7d464a7bfa0fcd4edf6c67837d14fb09c1c20516cf20b"}, + {file = "fonttools-4.42.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0a1466713e54bdbf5521f2f73eebfe727a528905ff5ec63cda40961b4b1eea95"}, + {file = "fonttools-4.42.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3fb2a69870bfe143ec20b039a1c8009e149dd7780dd89554cc8a11f79e5de86b"}, + {file = "fonttools-4.42.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae881e484702efdb6cf756462622de81d4414c454edfd950b137e9a7352b3cb9"}, + {file = "fonttools-4.42.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ec3246a088555629f9f0902f7412220c67340553ca91eb540cf247aacb1983"}, + {file = "fonttools-4.42.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ece1886d12bb36c48c00b2031518877f41abae317e3a55620d38e307d799b7e"}, + {file = "fonttools-4.42.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:10dac980f2b975ef74532e2a94bb00e97a95b4595fb7f98db493c474d5f54d0e"}, + {file = "fonttools-4.42.0-cp311-cp311-win32.whl", hash = "sha256:83b98be5d291e08501bd4fc0c4e0f8e6e05b99f3924068b17c5c9972af6fff84"}, + {file = "fonttools-4.42.0-cp311-cp311-win_amd64.whl", hash = "sha256:e35bed436726194c5e6e094fdfb423fb7afaa0211199f9d245e59e11118c576c"}, + {file = "fonttools-4.42.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c36c904ce0322df01e590ba814d5d69e084e985d7e4c2869378671d79662a7d4"}, + {file = "fonttools-4.42.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d54e600a2bcfa5cdaa860237765c01804a03b08404d6affcd92942fa7315ffba"}, + {file = "fonttools-4.42.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01cfe02416b6d416c5c8d15e30315cbcd3e97d1b50d3b34b0ce59f742ef55258"}, + {file = "fonttools-4.42.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f81ed9065b4bd3f4f3ce8e4873cd6a6b3f4e92b1eddefde35d332c6f414acc3"}, + {file = "fonttools-4.42.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:685a4dd6cf31593b50d6d441feb7781a4a7ef61e19551463e14ed7c527b86f9f"}, + {file = "fonttools-4.42.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:329341ba3d86a36e482610db56b30705384cb23bd595eac8cbb045f627778e9d"}, + {file = "fonttools-4.42.0-cp38-cp38-win32.whl", hash = "sha256:4655c480a1a4d706152ff54f20e20cf7609084016f1df3851cce67cef768f40a"}, + {file = "fonttools-4.42.0-cp38-cp38-win_amd64.whl", hash = "sha256:6bd7e4777bff1dcb7c4eff4786998422770f3bfbef8be401c5332895517ba3fa"}, + {file = "fonttools-4.42.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9b55d2a3b360e0c7fc5bd8badf1503ca1c11dd3a1cd20f2c26787ffa145a9c7"}, + {file = "fonttools-4.42.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0df8ef75ba5791e873c9eac2262196497525e3f07699a2576d3ab9ddf41cb619"}, + {file = "fonttools-4.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd2363ea7728496827658682d049ffb2e98525e2247ca64554864a8cc945568"}, + {file = "fonttools-4.42.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40673b2e927f7cd0819c6f04489dfbeb337b4a7b10fc633c89bf4f34ecb9620"}, + {file = "fonttools-4.42.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c8bf88f9e3ce347c716921804ef3a8330cb128284eb6c0b6c4b3574f3c580023"}, + {file = "fonttools-4.42.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:703101eb0490fae32baf385385d47787b73d9ea55253df43b487c89ec767e0d7"}, + {file = "fonttools-4.42.0-cp39-cp39-win32.whl", hash = "sha256:f0290ea7f9945174bd4dfd66e96149037441eb2008f3649094f056201d99e293"}, + {file = "fonttools-4.42.0-cp39-cp39-win_amd64.whl", hash = "sha256:ae7df0ae9ee2f3f7676b0ff6f4ebe48ad0acaeeeaa0b6839d15dbf0709f2c5ef"}, + {file = "fonttools-4.42.0-py3-none-any.whl", hash = "sha256:dfe7fa7e607f7e8b58d0c32501a3a7cac148538300626d1b930082c90ae7f6bd"}, + {file = "fonttools-4.42.0.tar.gz", hash = "sha256:614b1283dca88effd20ee48160518e6de275ce9b5456a3134d5f235523fc5065"}, ] [package.extras] @@ -627,20 +691,6 @@ files = [ {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"}, ] -[[package]] -name = "future-fstrings" -version = "1.2.0" -description = "A backport of fstrings to python<3.6" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "future_fstrings-1.2.0-py2.py3-none-any.whl", hash = "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63"}, - {file = "future_fstrings-1.2.0.tar.gz", hash = "sha256:6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089"}, -] - -[package.extras] -rewrite = ["tokenize-rt (>=3)"] - [[package]] name = "gitdb" version = "4.0.10" @@ -850,6 +900,44 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "ipython" +version = "8.14.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, + {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + [[package]] name = "isort" version = "5.12.0" @@ -867,6 +955,58 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib" plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] +[[package]] +name = "jedi" +version = "0.19.0" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, + {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsonpickle" +version = "3.0.1" +description = "Python library for serializing any arbitrary object graph into JSON" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonpickle-3.0.1-py2.py3-none-any.whl", hash = "sha256:130d8b293ea0add3845de311aaba55e6d706d0bb17bc123bd2c8baf8a39ac77c"}, + {file = "jsonpickle-3.0.1.tar.gz", hash = "sha256:032538804795e73b94ead410800ac387fdb6de98f8882ac957fcd247e3a85200"}, +] + +[package.extras] +docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"] +testing-libs = ["simplejson", "ujson"] + [[package]] name = "kiwisolver" version = "1.4.4" @@ -955,6 +1095,65 @@ files = [ {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, ] +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + [[package]] name = "matplotlib" version = "3.7.2" @@ -1016,6 +1215,20 @@ pillow = ">=6.2.0" pyparsing = ">=2.3.1,<3.1" python-dateutil = ">=2.7" +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + [[package]] name = "mccabe" version = "0.6.1" @@ -1187,36 +1400,36 @@ test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] name = "numpy" -version = "1.25.1" +version = "1.25.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d339465dff3eb33c701430bcb9c325b60354698340229e1dff97745e6b3efa"}, - {file = "numpy-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d736b75c3f2cb96843a5c7f8d8ccc414768d34b0a75f466c05f3a739b406f10b"}, - {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a90725800caeaa160732d6b31f3f843ebd45d6b5f3eec9e8cc287e30f2805bf"}, - {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c6c9261d21e617c6dc5eacba35cb68ec36bb72adcff0dee63f8fbc899362588"}, - {file = "numpy-1.25.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0def91f8af6ec4bb94c370e38c575855bf1d0be8a8fbfba42ef9c073faf2cf19"}, - {file = "numpy-1.25.1-cp310-cp310-win32.whl", hash = "sha256:fd67b306320dcadea700a8f79b9e671e607f8696e98ec255915c0c6d6b818503"}, - {file = "numpy-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1516db588987450b85595586605742879e50dcce923e8973f79529651545b57"}, - {file = "numpy-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b82655dd8efeea69dbf85d00fca40013d7f503212bc5259056244961268b66e"}, - {file = "numpy-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e8f6049c4878cb16960fbbfb22105e49d13d752d4d8371b55110941fb3b17800"}, - {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41a56b70e8139884eccb2f733c2f7378af06c82304959e174f8e7370af112e09"}, - {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5154b1a25ec796b1aee12ac1b22f414f94752c5f94832f14d8d6c9ac40bcca6"}, - {file = "numpy-1.25.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38eb6548bb91c421261b4805dc44def9ca1a6eef6444ce35ad1669c0f1a3fc5d"}, - {file = "numpy-1.25.1-cp311-cp311-win32.whl", hash = "sha256:791f409064d0a69dd20579345d852c59822c6aa087f23b07b1b4e28ff5880fcb"}, - {file = "numpy-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:c40571fe966393b212689aa17e32ed905924120737194b5d5c1b20b9ed0fb171"}, - {file = "numpy-1.25.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d7abcdd85aea3e6cdddb59af2350c7ab1ed764397f8eec97a038ad244d2d105"}, - {file = "numpy-1.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a180429394f81c7933634ae49b37b472d343cccb5bb0c4a575ac8bbc433722f"}, - {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d412c1697c3853c6fc3cb9751b4915859c7afe6a277c2bf00acf287d56c4e625"}, - {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20e1266411120a4f16fad8efa8e0454d21d00b8c7cee5b5ccad7565d95eb42dd"}, - {file = "numpy-1.25.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f76aebc3358ade9eacf9bc2bb8ae589863a4f911611694103af05346637df1b7"}, - {file = "numpy-1.25.1-cp39-cp39-win32.whl", hash = "sha256:247d3ffdd7775bdf191f848be8d49100495114c82c2bd134e8d5d075fb386a1c"}, - {file = "numpy-1.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:1d5d3c68e443c90b38fdf8ef40e60e2538a27548b39b12b73132456847f4b631"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:35a9527c977b924042170a0887de727cd84ff179e478481404c5dc66b4170009"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d3fe3dd0506a28493d82dc3cf254be8cd0d26f4008a417385cbf1ae95b54004"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:012097b5b0d00a11070e8f2e261128c44157a8689f7dedcf35576e525893f4fe"}, - {file = "numpy-1.25.1.tar.gz", hash = "sha256:9a3a9f3a61480cc086117b426a8bd86869c213fc4072e606f01c4e4b66eb92bf"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, + {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, + {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, + {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, + {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, + {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, + {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, + {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, + {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, + {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, + {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, ] [[package]] @@ -1352,6 +1565,21 @@ sql-other = ["SQLAlchemy (>=1.4.16)"] test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.6.3)"] +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + [[package]] name = "pathspec" version = "0.11.2" @@ -1377,6 +1605,17 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + [[package]] name = "pillow" version = "10.0.0" @@ -1487,6 +1726,20 @@ files = [ {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.39" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "psutil" version = "5.9.5" @@ -1524,6 +1777,20 @@ files = [ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "pyasn1" version = "0.5.0" @@ -1623,6 +1890,20 @@ files = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -1659,23 +1940,6 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] -[[package]] -name = "pytest-depends" -version = "1.0.1" -description = "Tests that depend on other tests" -optional = false -python-versions = "*" -files = [ - {file = "pytest-depends-1.0.1.tar.gz", hash = "sha256:90a28e2b87b75b18abd128c94015248544acac20e4392e9921e5a86f93319dfe"}, - {file = "pytest_depends-1.0.1-py3-none-any.whl", hash = "sha256:a1df072bcc93d77aca3f0946903f5fed8af2d9b0056db1dfc9ed5ac164ab0642"}, -] - -[package.dependencies] -colorama = "*" -future-fstrings = "*" -networkx = "*" -pytest = ">=3" - [[package]] name = "python-dateutil" version = "2.8.2" @@ -1715,6 +1979,22 @@ files = [ {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, ] +[[package]] +name = "pyvis" +version = "0.3.2" +description = "A Python network graph visualization library" +optional = false +python-versions = ">3.6" +files = [ + {file = "pyvis-0.3.2-py3-none-any.whl", hash = "sha256:5720c4ca8161dc5d9ab352015723abb7a8bb8fb443edeb07f7a322db34a97555"}, +] + +[package.dependencies] +ipython = ">=5.3.0" +jinja2 = ">=2.9.6" +jsonpickle = ">=1.4.1" +networkx = ">=1.11" + [[package]] name = "requests" version = "2.31.0" @@ -1801,6 +2081,25 @@ files = [ {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + [[package]] name = "starlette" version = "0.27.0" @@ -1849,6 +2148,21 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "traitlets" +version = "5.9.0" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.7" +files = [ + {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, + {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] + [[package]] name = "types-requests" version = "2.31.0.2" @@ -1912,6 +2226,17 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + [[package]] name = "wsproto" version = "1.2.0" @@ -2016,4 +2341,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c49ef31fd15da62773661a27bbd829e5d5bd0d0f45294f721a72eded670762e8" +content-hash = "793a224969f4cdc094de21061917863e1d0fb3a960d57bd2c3947c3d5d5040eb" diff --git a/pyproject.toml b/pyproject.toml index 98511ac1..b8569a9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ pytest = "^7.3.2" requests = "^2.31.0" openai = "^0.27.8" pydantic = "^1.10.9" -pytest-depends = "^1.0.1" python-dotenv = "^1.0.0" click = "^8.1.3" types-requests = "^2.31.0.1" @@ -23,7 +22,10 @@ helicone = "^1.0.6" matplotlib = "^3.7.2" pandas = "^2.0.3" gitpython = "^3.1.32" +networkx = "^3.1" +colorama = "^0.4.6" agent-protocol = "^0.1.2" +pyvis = "^0.3.2" [tool.poetry.group.dev.dependencies] flake8 = "^3.9.2" @@ -55,6 +57,10 @@ markers = [ "safety", "content_gen" ] +filterwarnings = [ + "ignore::pytest.PytestAssertRewriteWarning", + "ignore::matplotlib.MatplotlibDeprecationWarning" +] [tool.poetry.scripts] agbenchmark = "agbenchmark.start_benchmark:cli"