From a75ab1459dc6fb74f3d11100e19947f2fa77536b Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 25 Aug 2022 11:07:07 -0500 Subject: [PATCH] Remove deprecated `use_deprecated_directory_cli_args_semantics` option (#16630) Directories on the CLI now never mean the target `dir:dir`. [ci skip-rust] [ci skip-build-wheels] --- .../backend/explorer/graphql/query/targets.py | 4 +- .../backend/project_info/count_loc_test.py | 12 +-- .../backend/project_info/dependencies_test.py | 4 +- .../pants/backend/project_info/paths.py | 8 +- src/python/pants/base/specs.py | 11 +-- src/python/pants/base/specs_parser.py | 4 +- src/python/pants/bsp/util_rules/targets.py | 4 +- src/python/pants/core/goals/tailor.py | 86 ++----------------- src/python/pants/core/goals/tailor_test.py | 50 ----------- .../pants/engine/internals/engine_test.py | 4 +- .../pants/engine/internals/graph_test.py | 2 +- .../engine/internals/specs_rules_test.py | 17 +--- src/python/pants/init/specs_calculator.py | 3 - src/python/pants/option/global_options.py | 30 ------- src/python/pants/testutil/rule_runner.py | 4 +- 15 files changed, 27 insertions(+), 216 deletions(-) diff --git a/src/python/pants/backend/explorer/graphql/query/targets.py b/src/python/pants/backend/explorer/graphql/query/targets.py index 32e28c80244..2107c4e4814 100644 --- a/src/python/pants/backend/explorer/graphql/query/targets.py +++ b/src/python/pants/backend/explorer/graphql/query/targets.py @@ -166,9 +166,7 @@ async def targets(self, info: Info, query: Optional[TargetsQuery] = None) -> Lis req = GraphQLContext.request_state_from_info(info).product_request specs = ( specs_parser.parse_specs( - query.specs, - convert_dir_literal_to_address_literal=False, - description_of_origin="GraphQL targets `query.specs`", + query.specs, description_of_origin="GraphQL targets `query.specs`" ) if query is not None and query.specs else None diff --git a/src/python/pants/backend/project_info/count_loc_test.py b/src/python/pants/backend/project_info/count_loc_test.py index 96ca03dfdf6..f7cef7c7f1b 100644 --- a/src/python/pants/backend/project_info/count_loc_test.py +++ b/src/python/pants/backend/project_info/count_loc_test.py @@ -59,13 +59,15 @@ def test_count_loc(rule_runner: RuleRunner) -> None: { f"{py_dir}/foo.py": '# A comment.\n\nprint("some code")\n# Another comment.', f"{py_dir}/bar.py": '# A comment.\n\nprint("some more code")', - f"{py_dir}/BUILD": "python_sources()", + f"{py_dir}/BUILD": "python_sources(name='lib')", f"{elixir_dir}/foo.ex": 'IO.puts("Some elixir")\n# A comment', f"{elixir_dir}/ignored.ex": "# We do not expect this file to appear in counts.", - f"{elixir_dir}/BUILD": "elixir(sources=['foo.ex'])", + f"{elixir_dir}/BUILD": "elixir(name='lib', sources=['foo.ex'])", } ) - result = rule_runner.run_goal_rule(CountLinesOfCode, args=[py_dir, elixir_dir]) + result = rule_runner.run_goal_rule( + CountLinesOfCode, args=[f"{py_dir}:lib", f"{elixir_dir}:lib"] + ) assert result.exit_code == 0 assert_counts(result.stdout, "Python", num_files=2, blank=2, comment=3, code=2) assert_counts(result.stdout, "Elixir", comment=1, code=1) @@ -98,6 +100,6 @@ def test_files_without_owners(rule_runner: RuleRunner) -> None: def test_no_sources_exits_gracefully(rule_runner: RuleRunner) -> None: py_dir = "src/py/foo" - rule_runner.write_files({f"{py_dir}/BUILD": "python_sources()"}) - result = rule_runner.run_goal_rule(CountLinesOfCode, args=[py_dir]) + rule_runner.write_files({f"{py_dir}/BUILD": "python_sources(name='lib')"}) + result = rule_runner.run_goal_rule(CountLinesOfCode, args=[f"{py_dir}:lib"]) assert result == GoalRuleResult.noop() diff --git a/src/python/pants/backend/project_info/dependencies_test.py b/src/python/pants/backend/project_info/dependencies_test.py index cc68c1acccf..3b934ec62da 100644 --- a/src/python/pants/backend/project_info/dependencies_test.py +++ b/src/python/pants/backend/project_info/dependencies_test.py @@ -136,7 +136,7 @@ def test_python_dependencies(rule_runner: RuleRunner) -> None: assert_deps = partial(assert_dependencies, rule_runner) assert_deps( - specs=["some/other/target"], + specs=["some/other/target:target"], transitive=False, expected=["some/other/target/a.py"], ) @@ -146,7 +146,7 @@ def test_python_dependencies(rule_runner: RuleRunner) -> None: expected=["3rdparty/python:req2", "some/target/a.py"], ) assert_deps( - specs=["some/other/target"], + specs=["some/other/target:target"], transitive=True, expected=[ "3rdparty/python:req1", diff --git a/src/python/pants/backend/project_info/paths.py b/src/python/pants/backend/project_info/paths.py index 0aeba40935b..f5e70c8a1eb 100644 --- a/src/python/pants/backend/project_info/paths.py +++ b/src/python/pants/backend/project_info/paths.py @@ -20,7 +20,6 @@ TransitiveTargets, TransitiveTargetsRequest, ) -from pants.option.global_options import GlobalOptions from pants.option.option_types import StrOption @@ -80,9 +79,7 @@ def find_paths_breadth_first( @goal_rule -async def paths( - console: Console, paths_subsystem: PathsSubsystem, global_options: GlobalOptions -) -> PathsGoal: +async def paths(console: Console, paths_subsystem: PathsSubsystem) -> PathsGoal: path_from = paths_subsystem.from_ path_to = paths_subsystem.to @@ -95,7 +92,6 @@ async def paths( specs_parser = SpecsParser() - convert_dir_literals = global_options.use_deprecated_directory_cli_args_semantics from_tgts, to_tgts = await MultiGet( Get( Targets, @@ -103,7 +99,6 @@ async def paths( specs_parser.parse_specs( [path_from], description_of_origin="the option `--paths-from`", - convert_dir_literal_to_address_literal=convert_dir_literals, ), ), Get( @@ -112,7 +107,6 @@ async def paths( specs_parser.parse_specs( [path_to], description_of_origin="the option `--paths-to`", - convert_dir_literal_to_address_literal=convert_dir_literals, ), ), ) diff --git a/src/python/pants/base/specs.py b/src/python/pants/base/specs.py index b99c7c337e6..27c637d2799 100644 --- a/src/python/pants/base/specs.py +++ b/src/python/pants/base/specs.py @@ -114,10 +114,6 @@ class DirLiteralSpec(Spec): def __str__(self) -> str: return self.directory - def to_address_literal(self) -> AddressLiteralSpec: - """For now, `dir` can also be shorthand for `dir:dir`.""" - return AddressLiteralSpec(path_component=self.directory) - def matches_target_residence_dir(self, residence_dir: str) -> bool: return residence_dir == self.directory @@ -253,7 +249,7 @@ def create( specs: Iterable[Spec], *, description_of_origin: str, - convert_dir_literal_to_address_literal: bool, + convert_dir_literal_to_address_literal: bool = False, unmatched_glob_behavior: GlobMatchErrorBehavior = GlobMatchErrorBehavior.error, filter_by_global_options: bool = False, from_change_detection: bool = False, @@ -278,10 +274,7 @@ def create( elif isinstance(spec, FileGlobSpec): file_globs.append(spec) elif isinstance(spec, DirLiteralSpec): - if convert_dir_literal_to_address_literal: - address_literals.append(spec.to_address_literal()) - else: - dir_literals.append(spec) + dir_literals.append(spec) elif isinstance(spec, DirGlobSpec): dir_globs.append(spec) elif isinstance(spec, RecursiveGlobSpec): diff --git a/src/python/pants/base/specs_parser.py b/src/python/pants/base/specs_parser.py index 4efe6d16432..c5a863908a6 100644 --- a/src/python/pants/base/specs_parser.py +++ b/src/python/pants/base/specs_parser.py @@ -116,7 +116,7 @@ def parse_specs( specs: Iterable[str], *, description_of_origin: str, - convert_dir_literal_to_address_literal: bool, + convert_dir_literal_to_address_literal: bool = False, unmatched_glob_behavior: GlobMatchErrorBehavior = GlobMatchErrorBehavior.error, ) -> Specs: include_specs = [] @@ -131,14 +131,12 @@ def parse_specs( includes = RawSpecs.create( include_specs, description_of_origin=description_of_origin, - convert_dir_literal_to_address_literal=convert_dir_literal_to_address_literal, unmatched_glob_behavior=unmatched_glob_behavior, filter_by_global_options=True, ) ignores = RawSpecs.create( ignore_specs, description_of_origin=description_of_origin, - convert_dir_literal_to_address_literal=convert_dir_literal_to_address_literal, unmatched_glob_behavior=unmatched_glob_behavior, # By setting the below to False, we will end up matching some targets # that cannot have been resolved by the include specs. For example, if the user runs diff --git a/src/python/pants/bsp/util_rules/targets.py b/src/python/pants/bsp/util_rules/targets.py index e88ef307c0e..4b2e0b0b63f 100644 --- a/src/python/pants/bsp/util_rules/targets.py +++ b/src/python/pants/bsp/util_rules/targets.py @@ -137,9 +137,7 @@ class _ParseOneBSPMappingRequest: async def parse_one_bsp_mapping(request: _ParseOneBSPMappingRequest) -> BSPBuildTargetInternal: specs_parser = SpecsParser() specs = specs_parser.parse_specs( - request.definition.addresses, - description_of_origin=f"the BSP mapping {request.name}", - convert_dir_literal_to_address_literal=False, + request.definition.addresses, description_of_origin=f"the BSP mapping {request.name}" ).includes return BSPBuildTargetInternal(request.name, specs, request.definition) diff --git a/src/python/pants/core/goals/tailor.py b/src/python/pants/core/goals/tailor.py index c1413343561..d64560efc12 100644 --- a/src/python/pants/core/goals/tailor.py +++ b/src/python/pants/core/goals/tailor.py @@ -12,7 +12,7 @@ from dataclasses import dataclass from typing import Iterable, Iterator, Mapping, cast -from pants.base.specs import AncestorGlobSpec, RawSpecs, Spec, Specs +from pants.base.specs import AncestorGlobSpec, RawSpecs, Specs from pants.build_graph.address import Address from pants.engine.collection import DeduplicatedCollection from pants.engine.console import Console @@ -41,7 +41,6 @@ UnexpandedTargets, ) from pants.engine.unions import UnionMembership, union -from pants.option.global_options import GlobalOptions from pants.option.option_types import BoolOption, DictOption, StrListOption, StrOption from pants.source.filespec import FilespecMatcher from pants.util.docutil import bin_name, doc_url @@ -58,33 +57,17 @@ @dataclass(frozen=True) class PutativeTargetsRequest(metaclass=ABCMeta): dirs: tuple[str, ...] - deprecated_recursive_dirs: tuple[str, ...] = () def path_globs(self, *filename_globs: str) -> PathGlobs: - return PathGlobs( - globs=( - *(os.path.join(d, glob) for d in self.dirs for glob in filename_globs), - *( - os.path.join(d, "**", glob) - for d in self.deprecated_recursive_dirs - for glob in filename_globs - ), - ) - ) + return PathGlobs(os.path.join(d, glob) for d in self.dirs for glob in filename_globs) @dataclass(frozen=True) class PutativeTargetsSearchPaths: dirs: tuple[str, ...] - deprecated_recursive_dirs: tuple[str, ...] = () def path_globs(self, filename_glob: str) -> PathGlobs: - return PathGlobs( - globs=( - *(os.path.join(d, filename_glob) for d in self.dirs), - *(os.path.join(d, "**", filename_glob) for d in self.deprecated_recursive_dirs), - ) - ) + return PathGlobs(globs=(os.path.join(d, filename_glob) for d in self.dirs)) @memoized @@ -571,53 +554,6 @@ def make_content(bf_path: str, pts: Iterable[PutativeTarget]) -> FileContent: return EditedBuildFiles(new_digest, tuple(sorted(created)), tuple(sorted(updated))) -def specs_to_dirs(specs: RawSpecs) -> tuple[str, ...]: - """Extract cmd-line specs that look like directories. - - Error on all other specs. - - This is a hack that allows us to emulate "directory specs" while we deprecate the shorthand of - `dir` being `dir:dir`. - """ - dir_specs = [dir_spec.directory for dir_spec in specs.dir_literals] - other_specs: list[Spec] = [ - *specs.file_literals, - *specs.file_globs, - *specs.dir_globs, - *specs.recursive_globs, - *specs.ancestor_globs, - ] - for spec in specs.address_literals: - if spec.is_directory_shorthand: - dir_specs.append(spec.path_component) - else: - other_specs.append(spec) - if other_specs: - raise ValueError( - softwrap( - f""" - The global option `use_deprecated_cli_args_semantics` is set to `true`, so the - tailor goal is using deprecated semantics for CLI arguments. In this mode, the - tailor goal only accepts literal directories as arguments, which it will run - recursively on. You specified {', '.join(str(spec) for spec in other_specs)} - - To fix, either use the default value of `use_deprecated_cli_args_semantics` of - false, or rerun with - specifying only literal directories, e.g. `{bin_name()} tailor dir1 dir2`. If - changing `use_deprecated_cli_args_semantics` to false, you should specify which - directories to run on when using `tailor`: - - * `{bin_name()} tailor ::` to run on everything - * `{bin_name()} tailor dir::` to run on `dir` and subdirs - * `{bin_name()} tailor dir` to run on `dir` - * `{bin_name()} --changed-since=HEAD tailor` to only run on changed and new files - """ - ) - ) - # No specs at all means search the entire repo. - return tuple(dir_specs) or ("",) - - @goal_rule async def tailor( tailor_subsystem: TailorSubsystem, @@ -626,7 +562,6 @@ async def tailor( union_membership: UnionMembership, specs: Specs, build_file_options: BuildFileOptions, - global_options: GlobalOptions, ) -> TailorGoal: tailor_subsystem.validate_build_file_name(build_file_options.patterns) if not specs: @@ -647,20 +582,11 @@ async def tailor( ) return TailorGoal(exit_code=0) - dir_search_paths: tuple[str, ...] = () - recursive_search_paths: tuple[str, ...] = () - if global_options.use_deprecated_directory_cli_args_semantics: - recursive_search_paths = specs_to_dirs(specs.includes) - else: - specs_paths = await Get(SpecsPaths, Specs, specs) - dir_search_paths = tuple(sorted({os.path.dirname(f) for f in specs_paths.files})) + specs_paths = await Get(SpecsPaths, Specs, specs) + dir_search_paths = tuple(sorted({os.path.dirname(f) for f in specs_paths.files})) putative_targets_results = await MultiGet( - Get( - PutativeTargets, - PutativeTargetsRequest, - req_type(dir_search_paths, recursive_search_paths), - ) + Get(PutativeTargets, PutativeTargetsRequest, req_type(dir_search_paths)) for req_type in union_membership[PutativeTargetsRequest] ) putative_targets = PutativeTargets.merge(putative_targets_results) diff --git a/src/python/pants/core/goals/tailor_test.py b/src/python/pants/core/goals/tailor_test.py index 55eeefec2a7..e3910b6c233 100644 --- a/src/python/pants/core/goals/tailor_test.py +++ b/src/python/pants/core/goals/tailor_test.py @@ -10,7 +10,6 @@ import pytest -from pants.base.specs import AddressLiteralSpec, DirLiteralSpec, FileLiteralSpec, RawSpecs from pants.core.goals import tailor from pants.core.goals.tailor import ( AllOwnedSources, @@ -26,7 +25,6 @@ default_sources_for_target_type, group_by_dir, make_content_str, - specs_to_dirs, ) from pants.core.util_rules import source_files from pants.engine.fs import DigestContents, FileContent, PathGlobs, Paths @@ -431,54 +429,6 @@ def test_group_by_dir() -> None: } == group_by_dir(paths) -def test_specs_to_dirs() -> None: - assert specs_to_dirs(RawSpecs(description_of_origin="tests")) == ("",) - assert specs_to_dirs( - RawSpecs( - address_literals=(AddressLiteralSpec("src/python/foo"),), description_of_origin="tests" - ) - ) == ("src/python/foo",) - assert specs_to_dirs( - RawSpecs(dir_literals=(DirLiteralSpec("src/python/foo"),), description_of_origin="tests") - ) == ("src/python/foo",) - assert specs_to_dirs( - RawSpecs( - address_literals=( - AddressLiteralSpec("src/python/foo"), - AddressLiteralSpec("src/python/bar"), - ), - description_of_origin="tests", - ) - ) == ("src/python/foo", "src/python/bar") - - with pytest.raises(ValueError): - specs_to_dirs( - RawSpecs( - file_literals=(FileLiteralSpec("src/python/foo.py"),), description_of_origin="tests" - ) - ) - - with pytest.raises(ValueError): - specs_to_dirs( - RawSpecs( - address_literals=(AddressLiteralSpec("src/python/bar", "tgt"),), - description_of_origin="tests", - ) - ) - - with pytest.raises(ValueError): - specs_to_dirs( - RawSpecs( - address_literals=( - AddressLiteralSpec( - "src/python/bar", target_component=None, generated_component="gen" - ), - ), - description_of_origin="tests", - ) - ) - - def test_tailor_rule_write_mode(rule_runner: RuleRunner) -> None: rule_runner.write_files( { diff --git a/src/python/pants/engine/internals/engine_test.py b/src/python/pants/engine/internals/engine_test.py index 18fab013df0..63f334cb014 100644 --- a/src/python/pants/engine/internals/engine_test.py +++ b/src/python/pants/engine/internals/engine_test.py @@ -927,9 +927,7 @@ def test_streaming_workunits_expanded_specs(run_tracker: RunTracker) -> None: } ) specs = SpecsParser().parse_specs( - ["src/python/somefiles::", "src/python/others/b.py"], - convert_dir_literal_to_address_literal=False, - description_of_origin="tests", + ["src/python/somefiles::", "src/python/others/b.py"], description_of_origin="tests" ) class Callback(WorkunitsCallback): diff --git a/src/python/pants/engine/internals/graph_test.py b/src/python/pants/engine/internals/graph_test.py index a28fa3f975f..20941713c5e 100644 --- a/src/python/pants/engine/internals/graph_test.py +++ b/src/python/pants/engine/internals/graph_test.py @@ -903,7 +903,7 @@ def assert_generated( # TODO: Adjust the `TransitiveTargets` API to expose the complete mapping. # see https://github.com/pantsbuild/pants/issues/11270 specs = SpecsParser(rule_runner.build_root).parse_specs( - ["::"], convert_dir_literal_to_address_literal=False, description_of_origin="tests" + ["::"], description_of_origin="tests" ) addresses = rule_runner.request(Addresses, [specs]) dependency_mapping = rule_runner.request( diff --git a/src/python/pants/engine/internals/specs_rules_test.py b/src/python/pants/engine/internals/specs_rules_test.py index cb683cbbeaa..10c23911a6a 100644 --- a/src/python/pants/engine/internals/specs_rules_test.py +++ b/src/python/pants/engine/internals/specs_rules_test.py @@ -153,7 +153,6 @@ def resolve_raw_specs_without_file_owners( specs_obj = RawSpecs.create( specs, filter_by_global_options=True, - convert_dir_literal_to_address_literal=False, unmatched_glob_behavior=( GlobMatchErrorBehavior.ignore if ignore_nonexistent else GlobMatchErrorBehavior.error ), @@ -533,7 +532,6 @@ def resolve_raw_specs_with_only_file_owners( specs_obj = RawSpecs.create( specs, filter_by_global_options=True, - convert_dir_literal_to_address_literal=True, unmatched_glob_behavior=( GlobMatchErrorBehavior.ignore if ignore_nonexistent else GlobMatchErrorBehavior.error ), @@ -641,7 +639,6 @@ def test_resolve_addresses_from_raw_specs(rule_runner: RuleRunner) -> None: multiple_files_specs = ["multiple_files/f2.txt", "multiple_files:multiple_files"] specs = SpecsParser(rule_runner.build_root).parse_specs( [*no_interaction_specs, *multiple_files_specs], - convert_dir_literal_to_address_literal=False, description_of_origin="tests", ) @@ -672,9 +669,7 @@ def test_resolve_addresses_from_specs(rule_runner: RuleRunner) -> None: ) def assert_resolved(specs: Iterable[str], expected: set[str]) -> None: - specs_obj = SpecsParser().parse_specs( - specs, convert_dir_literal_to_address_literal=False, description_of_origin="tests" - ) + specs_obj = SpecsParser().parse_specs(specs, description_of_origin="tests") result = rule_runner.request(Addresses, [specs_obj]) assert {addr.spec for addr in result} == expected @@ -805,9 +800,7 @@ def test_resolve_specs_paths(rule_runner: RuleRunner) -> None: def assert_paths( specs: Iterable[str], expected_files: set[str], expected_dirs: set[str] ) -> None: - specs_obj = SpecsParser().parse_specs( - specs, convert_dir_literal_to_address_literal=False, description_of_origin="tests" - ) + specs_obj = SpecsParser().parse_specs(specs, description_of_origin="tests") result = rule_runner.request(SpecsPaths, [specs_obj]) assert set(result.files) == expected_files assert set(result.dirs) == expected_dirs @@ -914,11 +907,7 @@ def find_valid_field_sets( [ request, Specs( - includes=RawSpecs.create( - specs, - convert_dir_literal_to_address_literal=True, - description_of_origin="tests", - ), + includes=RawSpecs.create(specs, description_of_origin="tests"), ignores=RawSpecs(description_of_origin="tests"), ), ], diff --git a/src/python/pants/init/specs_calculator.py b/src/python/pants/init/specs_calculator.py index 1a462c7886e..e7c89159243 100644 --- a/src/python/pants/init/specs_calculator.py +++ b/src/python/pants/init/specs_calculator.py @@ -35,9 +35,6 @@ def calculate_specs( options.specs, description_of_origin="CLI arguments", unmatched_glob_behavior=unmatched_cli_globs, - convert_dir_literal_to_address_literal=( - global_options.use_deprecated_directory_cli_args_semantics - ), ) changed_options = ChangedOptions.from_options(options.for_scope("changed")) diff --git a/src/python/pants/option/global_options.py b/src/python/pants/option/global_options.py index 4b36e312dca..50b172b6292 100644 --- a/src/python/pants/option/global_options.py +++ b/src/python/pants/option/global_options.py @@ -1710,36 +1710,6 @@ class GlobalOptions(BootstrapOptions, Subsystem): advanced=True, ) - use_deprecated_directory_cli_args_semantics = BoolOption( - default=False, - help=softwrap( - f""" - If true, Pants will use the old, deprecated semantics for directory arguments like - `{bin_name()} test dir`: directories are shorthand for the target `dir:dir`, i.e. the - target that leaves off `name=`. - - If false, Pants will use the new semantics: directory arguments will match all files - and targets in the directory, e.g. `{bin_name()} test dir` will run all tests in `dir`. - - This also impacts the behavior of the `tailor` goal. If this option is true, - `{bin_name()} tailor dir` will run over `dir` and all recursive sub-directories. If - false, specifying a directory will only add targets for that directory. - """ - ), - removal_version="2.15.0.dev0", - removal_hint=softwrap( - f""" - If `use_deprecated_directory_cli_args_semantics` is already set explicitly to `false`, - simply delete the option from `pants.toml` because `false` is now the default. - - If set to true, removing the option will cause directory arguments like `{bin_name()} - test project/dir` to now match all files and targets in the directory, whereas before - it matched the target `project/dir:dir`. To keep the old semantics, use the explicit - address syntax. - """ - ), - ) - use_deprecated_pex_binary_run_semantics = BoolOption( default=False, help=softwrap( diff --git a/src/python/pants/testutil/rule_runner.py b/src/python/pants/testutil/rule_runner.py index 804e358e43c..df8d0c5f53f 100644 --- a/src/python/pants/testutil/rule_runner.py +++ b/src/python/pants/testutil/rule_runner.py @@ -336,9 +336,7 @@ def run_goal_rule( [GlobalOptions.get_scope_info(), goal.subsystem_cls.get_scope_info()] ).specs specs = SpecsParser(self.build_root).parse_specs( - raw_specs, - convert_dir_literal_to_address_literal=True, - description_of_origin="RuleRunner.run_goal_rule()", + raw_specs, description_of_origin="RuleRunner.run_goal_rule()" ) stdout, stderr = StringIO(), StringIO()