File: test_plugin_manager.py

package info (click to toggle)
poetry 2.3.2%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 10,636 kB
  • sloc: python: 56,035; sh: 128; makefile: 100; ansic: 49
file content (627 lines) | stat: -rw-r--r-- 20,932 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
from __future__ import annotations

import shutil
import sys

from pathlib import Path
from typing import TYPE_CHECKING
from typing import ClassVar
from typing import Protocol

import pytest

from cleo.io.buffered_io import BufferedIO
from cleo.io.outputs.output import Verbosity
from poetry.core.constraints.version import Version
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.file_dependency import FileDependency
from poetry.core.packages.package import Package
from poetry.core.packages.project_package import ProjectPackage

from poetry.factory import Factory
from poetry.installation.wheel_installer import WheelInstaller
from poetry.packages.locker import Locker
from poetry.plugins import ApplicationPlugin
from poetry.plugins import Plugin
from poetry.plugins.plugin_manager import PluginManager
from poetry.plugins.plugin_manager import ProjectPluginCache
from poetry.poetry import Poetry
from poetry.puzzle.exceptions import SolverProblemError
from poetry.repositories import Repository
from poetry.repositories import RepositoryPool
from poetry.repositories.installed_repository import InstalledRepository
from tests.helpers import mock_metadata_entry_points


if TYPE_CHECKING:
    from cleo.io.io import IO
    from pytest_mock import MockerFixture

    from poetry.console.commands.command import Command
    from poetry.utils.env import Env
    from tests.conftest import Config
    from tests.types import FixtureDirGetter


class ManagerFactory(Protocol):
    def __call__(self, group: str = Plugin.group) -> PluginManager: ...


class MyPlugin(Plugin):
    def activate(self, poetry: Poetry, io: IO) -> None:
        io.write_line("Setting readmes")
        poetry.package.readmes = (Path("README.md"),)


class MyCommandPlugin(ApplicationPlugin):
    commands: ClassVar[list[type[Command]]] = []


class InvalidPlugin:
    group = "poetry.plugin"

    def activate(self, poetry: Poetry, io: IO) -> None:
        io.write_line("Updating version")
        poetry.package.version = Version.parse("9.9.9")


@pytest.fixture
def repo() -> Repository:
    repo = Repository("repo")
    repo.add_package(Package("my-other-plugin", "1.0"))
    for version in ("1.0", "2.0"):
        package = Package("my-application-plugin", version)
        package.add_dependency(Dependency("some-lib", version))
        repo.add_package(package)
        repo.add_package(Package("some-lib", version))
    return repo


@pytest.fixture
def pool(repo: Repository) -> RepositoryPool:
    pool = RepositoryPool()
    pool.add_repository(repo)

    return pool


@pytest.fixture
def poetry(fixture_dir: FixtureDirGetter, config: Config) -> Poetry:
    project_path = fixture_dir("simple_project")
    poetry = Poetry(
        project_path / "pyproject.toml",
        {},
        ProjectPackage("simple-project", "1.2.3"),
        Locker(project_path / "poetry.lock", {}),
        config,
    )

    return poetry


@pytest.fixture
def poetry_with_plugins(
    fixture_dir: FixtureDirGetter, pool: RepositoryPool, tmp_path: Path
) -> Poetry:
    orig_path = fixture_dir("project_plugins")
    project_path = tmp_path / "project"
    project_path.mkdir()
    shutil.copy(orig_path / "pyproject.toml", project_path / "pyproject.toml")
    poetry = Factory().create_poetry(project_path)
    poetry.set_pool(pool)
    return poetry


@pytest.fixture()
def io() -> BufferedIO:
    return BufferedIO()


@pytest.fixture(autouse=True)
def mock_sys_path(mocker: MockerFixture) -> None:
    sys_path_copy = sys.path.copy()
    mocker.patch("poetry.plugins.plugin_manager.sys.path", new=sys_path_copy)


@pytest.fixture()
def manager_factory(poetry: Poetry, io: BufferedIO) -> ManagerFactory:
    def _manager(group: str = Plugin.group) -> PluginManager:
        return PluginManager(group)

    return _manager


@pytest.fixture
def with_my_plugin(mocker: MockerFixture) -> None:
    mock_metadata_entry_points(mocker, MyPlugin)


@pytest.fixture
def with_invalid_plugin(mocker: MockerFixture) -> None:
    mock_metadata_entry_points(mocker, InvalidPlugin)


def test_load_plugins_and_activate(
    manager_factory: ManagerFactory,
    poetry: Poetry,
    io: BufferedIO,
    with_my_plugin: None,
) -> None:
    manager = manager_factory()
    manager.load_plugins()
    manager.activate(poetry, io)

    assert poetry.package.readmes == (Path("README.md"),)
    assert io.fetch_output() == "Setting readmes\n"


def test_load_plugins_with_invalid_plugin(
    manager_factory: ManagerFactory,
    poetry: Poetry,
    io: BufferedIO,
    with_invalid_plugin: None,
) -> None:
    manager = manager_factory()

    with pytest.raises(ValueError):
        manager.load_plugins()


def test_add_project_plugin_path(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    system_env: Env,
    fixture_dir: FixtureDirGetter,
) -> None:
    dist_info_1 = "my_application_plugin-1.0.dist-info"
    dist_info_2 = "my_application_plugin-2.0.dist-info"
    cache = ProjectPluginCache(poetry_with_plugins, io)
    shutil.copytree(
        fixture_dir("project_plugins") / dist_info_1, cache._path / dist_info_1
    )
    shutil.copytree(
        fixture_dir("project_plugins") / dist_info_2, system_env.purelib / dist_info_2
    )

    assert {
        f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages
    } == {"my-application-plugin 2.0"}

    PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent)

    assert {
        f"{p.name} {p.version}" for p in InstalledRepository.load(system_env).packages
    } == {"my-application-plugin 1.0"}


def test_add_project_plugin_path_addsitedir_called(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    mocker: MockerFixture,
) -> None:
    """Test that addsitedir is called when plugin path exists."""
    cache = ProjectPluginCache(poetry_with_plugins, io)
    cache._path.mkdir(parents=True, exist_ok=True)

    mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")

    PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent)

    # sys.path is mocked, so we can check it was modified
    assert str(cache._path) in sys.path
    assert sys.path[0] == str(cache._path)
    mock_addsitedir.assert_called_once_with(str(cache._path))


def test_add_project_plugin_path_no_addsitedir_when_path_missing(
    poetry_with_plugins: Poetry,
    mocker: MockerFixture,
) -> None:
    """Test that addsitedir is not called when plugin path doesn't exist."""
    cache = ProjectPluginCache(poetry_with_plugins, BufferedIO())
    # Ensure the plugin path does not exist
    if cache._path.exists():
        shutil.rmtree(cache._path)

    mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")
    initial_sys_path = sys.path.copy()

    PluginManager.add_project_plugin_path(poetry_with_plugins.pyproject_path.parent)

    assert sys.path == initial_sys_path
    mock_addsitedir.assert_not_called()


def test_add_project_plugin_path_no_pyproject(
    tmp_path: Path,
    mocker: MockerFixture,
) -> None:
    """Test that no action is taken when pyproject.toml is missing."""
    mock_addsitedir = mocker.patch("poetry.plugins.plugin_manager.addsitedir")
    initial_sys_path = sys.path.copy()

    # Call with a directory that has no pyproject.toml
    PluginManager.add_project_plugin_path(tmp_path)

    assert sys.path == initial_sys_path
    mock_addsitedir.assert_not_called()


def test_ensure_plugins_no_plugins_no_output(poetry: Poetry, io: BufferedIO) -> None:
    PluginManager.ensure_project_plugins(poetry, io)

    assert not (poetry.pyproject_path.parent / ProjectPluginCache.PATH).exists()
    assert io.fetch_output() == ""
    assert io.fetch_error() == ""


def test_ensure_plugins_no_plugins_existing_cache_is_removed(
    poetry: Poetry, io: BufferedIO
) -> None:
    plugin_path = poetry.pyproject_path.parent / ProjectPluginCache.PATH
    plugin_path.mkdir(parents=True)

    PluginManager.ensure_project_plugins(poetry, io)

    assert not plugin_path.exists()
    assert io.fetch_output() == (
        "No project plugins defined. Removing the project's plugin cache\n\n"
    )
    assert io.fetch_error() == ""


@pytest.mark.parametrize("debug_out", [False, True])
def test_ensure_plugins_no_output_if_fresh(
    poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool
) -> None:
    io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL)
    cache = ProjectPluginCache(poetry_with_plugins, io)
    cache._write_config()

    cache.ensure_plugins()

    assert cache._config_file.exists()
    assert (
        cache._gitignore_file.exists()
        and cache._gitignore_file.read_text(encoding="utf-8") == "*"
    )
    assert io.fetch_output() == (
        "The project's plugin cache is up to date.\n\n" if debug_out else ""
    )
    assert io.fetch_error() == ""


@pytest.mark.parametrize("debug_out", [False, True])
def test_ensure_plugins_ignore_irrelevant_markers(
    poetry_with_plugins: Poetry, io: BufferedIO, debug_out: bool
) -> None:
    io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL)
    poetry_with_plugins.local_config["requires-plugins"] = {
        "irrelevant": {"version": "1.0", "markers": "python_version < '3'"}
    }
    cache = ProjectPluginCache(poetry_with_plugins, io)

    cache.ensure_plugins()

    assert cache._config_file.exists()
    assert (
        cache._gitignore_file.exists()
        and cache._gitignore_file.read_text(encoding="utf-8") == "*"
    )
    assert io.fetch_output() == (
        "No relevant project plugins for Poetry's environment defined.\n\n"
        if debug_out
        else ""
    )
    assert io.fetch_error() == ""


def test_ensure_plugins_remove_outdated(
    poetry_with_plugins: Poetry, io: BufferedIO, fixture_dir: FixtureDirGetter
) -> None:
    # Test with irrelevant plugins because this is the first return
    # where it is relevant that an existing cache is removed.
    poetry_with_plugins.local_config["requires-plugins"] = {
        "irrelevant": {"version": "1.0", "markers": "python_version < '3'"}
    }
    fixture_path = fixture_dir("project_plugins")
    cache = ProjectPluginCache(poetry_with_plugins, io)
    cache._path.mkdir(parents=True)
    dist_info = "my_application_plugin-1.0.dist-info"
    shutil.copytree(fixture_path / dist_info, cache._path / dist_info)
    cache._config_file.touch()

    cache.ensure_plugins()

    assert cache._config_file.exists()
    assert not (cache._path / dist_info).exists()
    assert io.fetch_output() == (
        "Removing the project's plugin cache because it is outdated\n"
    )
    assert io.fetch_error() == ""


def test_ensure_plugins_ignore_already_installed_in_system_env(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    system_env: Env,
    fixture_dir: FixtureDirGetter,
) -> None:
    fixture_path = fixture_dir("project_plugins")
    for dist_info in (
        "my_application_plugin-2.0.dist-info",
        "my_other_plugin-1.0.dist-info",
    ):
        shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info)
    cache = ProjectPluginCache(poetry_with_plugins, io)

    cache.ensure_plugins()

    assert cache._config_file.exists()
    assert (
        cache._gitignore_file.exists()
        and cache._gitignore_file.read_text(encoding="utf-8") == "*"
    )
    assert io.fetch_output() == (
        "Ensuring that the Poetry plugins required by the project are available...\n"
        "All required plugins have already been installed in Poetry's environment.\n\n"
    )
    assert io.fetch_error() == ""


def test_ensure_plugins_install_missing_plugins(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    system_env: Env,
    fixture_dir: FixtureDirGetter,
    mocker: MockerFixture,
) -> None:
    cache = ProjectPluginCache(poetry_with_plugins, io)
    install_spy = mocker.spy(cache, "_install")
    execute_mock = mocker.patch(
        "poetry.plugins.plugin_manager.Installer._execute", return_value=0
    )

    cache.ensure_plugins()

    install_spy.assert_called_once_with(
        [
            Dependency("my-application-plugin", ">=2.0"),
            Dependency("my-other-plugin", ">=1.0"),
        ],
        system_env,
        [],
    )
    execute_mock.assert_called_once()
    assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [
        "<Install some-lib (2.0)>",
        "<Install my-application-plugin (2.0)>",
        "<Install my-other-plugin (1.0)>",
    ]
    assert cache._config_file.exists()
    assert (
        cache._gitignore_file.exists()
        and cache._gitignore_file.read_text(encoding="utf-8") == "*"
    )
    assert io.fetch_output() == (
        "Ensuring that the Poetry plugins required by the project are available...\n"
        "The following Poetry plugins are required by the project"
        " but are not installed in Poetry's environment:\n"
        "  - my-application-plugin (>=2.0)\n"
        "  - my-other-plugin (>=1.0)\n"
        "Installing Poetry plugins only for the current project...\n"
        "Updating dependencies\n"
        "Resolving dependencies...\n\n"
        "Writing lock file\n\n"
    )
    assert io.fetch_error() == ""


def test_ensure_plugins_install_only_missing_plugins(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    system_env: Env,
    fixture_dir: FixtureDirGetter,
    mocker: MockerFixture,
) -> None:
    fixture_path = fixture_dir("project_plugins")
    for dist_info in (
        "my_application_plugin-2.0.dist-info",
        "some_lib-2.0.dist-info",
    ):
        shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info)
    cache = ProjectPluginCache(poetry_with_plugins, io)
    install_spy = mocker.spy(cache, "_install")
    execute_mock = mocker.patch(
        "poetry.plugins.plugin_manager.Installer._execute", return_value=0
    )

    cache.ensure_plugins()

    install_spy.assert_called_once_with(
        [Dependency("my-other-plugin", ">=1.0")],
        system_env,
        [Package("my-application-plugin", "2.0"), Package("some-lib", "2.0")],
    )
    execute_mock.assert_called_once()
    assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [
        "<Install my-other-plugin (1.0)>"
    ]
    assert cache._config_file.exists()
    assert (
        cache._gitignore_file.exists()
        and cache._gitignore_file.read_text(encoding="utf-8") == "*"
    )
    assert io.fetch_output() == (
        "Ensuring that the Poetry plugins required by the project are available...\n"
        "The following Poetry plugins are required by the project"
        " but are not installed in Poetry's environment:\n"
        "  - my-other-plugin (>=1.0)\n"
        "Installing Poetry plugins only for the current project...\n"
        "Updating dependencies\n"
        "Resolving dependencies...\n\n"
        "Writing lock file\n\n"
    )
    assert io.fetch_error() == ""


@pytest.mark.parametrize("debug_out", [False, True])
def test_ensure_plugins_install_overwrite_wrong_version_plugins(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    system_env: Env,
    fixture_dir: FixtureDirGetter,
    mocker: MockerFixture,
    debug_out: bool,
) -> None:
    io.set_verbosity(Verbosity.DEBUG if debug_out else Verbosity.NORMAL)
    fixture_path = fixture_dir("project_plugins")
    for dist_info in (
        "my_application_plugin-1.0.dist-info",
        "some_lib-2.0.dist-info",
    ):
        shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info)
    cache = ProjectPluginCache(poetry_with_plugins, io)
    install_spy = mocker.spy(cache, "_install")
    execute_mock = mocker.patch(
        "poetry.plugins.plugin_manager.Installer._execute", return_value=0
    )

    cache.ensure_plugins()

    install_spy.assert_called_once_with(
        [
            Dependency("my-application-plugin", ">=2.0"),
            Dependency("my-other-plugin", ">=1.0"),
        ],
        system_env,
        [Package("some-lib", "2.0")],
    )
    execute_mock.assert_called_once()
    assert [repr(op) for op in execute_mock.call_args.args[0] if not op.skipped] == [
        "<Install my-application-plugin (2.0)>",
        "<Install my-other-plugin (1.0)>",
    ]
    assert cache._config_file.exists()
    assert (
        cache._gitignore_file.exists()
        and cache._gitignore_file.read_text(encoding="utf-8") == "*"
    )
    start = (
        "Ensuring that the Poetry plugins required by the project are available...\n"
    )
    opt = (
        "The following Poetry plugins are required by the project"
        " but are not satisfied by the installed versions:\n"
        "  - my-application-plugin (>=2.0)\n"
        "    installed: my-application-plugin (1.0)\n"
    )
    end = (
        "The following Poetry plugins are required by the project"
        " but are not installed in Poetry's environment:\n"
        "  - my-application-plugin (>=2.0)\n"
        "  - my-other-plugin (>=1.0)\n"
        "Installing Poetry plugins only for the current project...\n"
    )
    expected = (start + opt + end) if debug_out else (start + end)
    assert io.fetch_output().startswith(expected)
    assert io.fetch_error() == ""


def test_ensure_plugins_pins_other_installed_packages(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    system_env: Env,
    fixture_dir: FixtureDirGetter,
    mocker: MockerFixture,
) -> None:
    fixture_path = fixture_dir("project_plugins")
    for dist_info in (
        "my_application_plugin-1.0.dist-info",
        "some_lib-1.0.dist-info",
    ):
        shutil.copytree(fixture_path / dist_info, system_env.purelib / dist_info)
    cache = ProjectPluginCache(poetry_with_plugins, io)
    install_spy = mocker.spy(cache, "_install")
    execute_mock = mocker.patch(
        "poetry.plugins.plugin_manager.Installer._execute", return_value=0
    )

    with pytest.raises(SolverProblemError):
        cache.ensure_plugins()

    install_spy.assert_called_once_with(
        [
            Dependency("my-application-plugin", ">=2.0"),
            Dependency("my-other-plugin", ">=1.0"),
        ],
        system_env,
        # pinned because it might be a dependency of another plugin or Poetry itself
        [Package("some-lib", "1.0")],
    )
    execute_mock.assert_not_called()
    assert not cache._config_file.exists()
    assert (
        cache._gitignore_file.exists()
        and cache._gitignore_file.read_text(encoding="utf-8") == "*"
    )
    assert io.fetch_output() == (
        "Ensuring that the Poetry plugins required by the project are available...\n"
        "The following Poetry plugins are required by the project"
        " but are not installed in Poetry's environment:\n"
        "  - my-application-plugin (>=2.0)\n"
        "  - my-other-plugin (>=1.0)\n"
        "Installing Poetry plugins only for the current project...\n"
        "Updating dependencies\n"
        "Resolving dependencies...\n"
    )
    assert io.fetch_error() == ""


@pytest.mark.parametrize("other_version", [False, True])
def test_project_plugins_are_installed_in_project_folder(
    poetry_with_plugins: Poetry,
    io: BufferedIO,
    system_env: Env,
    fixture_dir: FixtureDirGetter,
    tmp_path: Path,
    other_version: bool,
) -> None:
    orig_purelib = system_env.purelib
    orig_platlib = system_env.platlib

    # make sure that the path dependency is on the same drive (for Windows tests in CI)
    orig_wheel_path = (
        fixture_dir("wheel_with_no_requires_dist") / "demo-0.1.0-py2.py3-none-any.whl"
    )
    wheel_path = tmp_path / orig_wheel_path.name
    shutil.copy(orig_wheel_path, wheel_path)

    if other_version:
        WheelInstaller(system_env).install(wheel_path)
        dist_info = orig_purelib / "demo-0.1.0.dist-info"
        metadata = dist_info / "METADATA"
        metadata.write_text(
            metadata.read_text(encoding="utf-8").replace("0.1.0", "0.1.2"),
            encoding="utf-8",
        )
        dist_info.rename(orig_purelib / "demo-0.1.2.dist-info")

    cache = ProjectPluginCache(poetry_with_plugins, io)

    # just use a file dependency so that we do not have to set up a repository
    cache._install([FileDependency("demo", wheel_path)], system_env, [])

    project_site_packages = [p.name for p in cache._path.iterdir()]
    assert "demo" in project_site_packages
    assert "demo-0.1.0.dist-info" in project_site_packages

    orig_site_packages = [p.name for p in orig_purelib.iterdir()]
    if other_version:
        assert "demo" in orig_site_packages
        assert "demo-0.1.2.dist-info" in orig_site_packages
        assert "demo-0.1.0.dist-info" not in orig_site_packages
    else:
        assert not any(p.startswith("demo") for p in orig_site_packages)
    if orig_platlib != orig_purelib:
        assert not any(p.name.startswith("demo") for p in orig_platlib.iterdir())