Repository: omni-us/jsonargparse
Pull request #228 (2023-01-09 07:55:50)

Deprecations related to Path types
## What does this PR do?

- Path's skip_check parameter is now deprecated and will be removed in v5.0.0.
- Modifying Path attributes is deprecated, in v5.0.0 they will be properties without a setter.
- ActionPathList is now deprecated and will be removed in v5.0.0.

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/master/CONTRIBUTING.rst)?
- [x] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


codecov-commenter: # [Codecov](https://codecov.io/gh/omni-us/jsonargparse/pull/228?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
Base: **100.00**% // Head: **100.00**% // No change to project coverage :thumbsup:
> Coverage data is based on head [(`ec0e7e6`)](https://codecov.io/gh/omni-us/jsonargparse/pull/228?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) compared to base [(`415cfa4`)](https://codecov.io/gh/omni-us/jsonargparse/commit/415cfa4a658275823c96ac7f8767131e0e89284c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##            master      #228   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           20        20           
  Lines         5080      5145   +65     
=========================================
+ Hits          5080      5145   +65     
```

| Flag | Coverage Δ | |
|---|---|---|
| py3.10 | `87.77% <95.45%> (+0.13%)` | :arrow_up: |
| py3.10_all | `99.88% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.6 | `87.41% <95.09%> (+0.12%)` | :arrow_up: |
| py3.6_all | `99.62% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.7 | `87.35% <94.84%> (+0.04%)` | :arrow_up: |
| py3.7_all | `99.80% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.8 | `87.75% <95.45%> (+0.13%)` | :arrow_up: |
| py3.8_all | `99.84% <100.00%> (+<0.01%)` | :arrow_up: |
| py3.9 | `87.75% <95.45%> (+0.13%)` | :arrow_up: |
| py3.9_all | `99.86% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/omni-us/jsonargparse/pull/228?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/deprecated.py](https://codecov.io/gh/omni-us/jsonargparse/pull/228?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL2RlcHJlY2F0ZWQucHk=) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/typing.py](https://codecov.io/gh/omni-us/jsonargparse/pull/228?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL3R5cGluZy5weQ==) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/util.py](https://codecov.io/gh/omni-us/jsonargparse/pull/228?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL3V0aWwucHk=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/omni-us/jsonargparse/pull/228?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=228)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=228) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=228&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=228&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=228)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=228&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=228) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=228&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=228&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml
new file mode 100644
index 0000000..5ef448a
--- /dev/null
+++ b/.github/codeql-config.yml
@@ -0,0 +1,3 @@
+query-filters:
+- exclude:
+    id: py/cyclic-import
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index cd8b72a..3be7962 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -31,6 +31,7 @@ jobs:
         with:
           languages: ${{ matrix.language }}
           queries: +security-and-quality
+          config-file: ./.github/codeql-config.yml
 
       - name: Autobuild
         uses: github/codeql-action/autobuild@v2
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 8867dfd..efeae4d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,20 @@ The semantic versioning only considers the public API as described in
 paths are considered internals and can change in minor and patch releases.
 
 
+v4.20.0 (2023-01-??)
+--------------------
+
+Deprecated
+^^^^^^^^^^
+- Path ``skip_check`` parameter is deprecated and will be removed in v5.0.0.
+  Instead use as type ``str`` or ``os.PathLike``.
+- Modifying Path attributes is deprecated. In v5.0.0 they will be properties
+  without a setter and two renamed: ``rel_path -> relative`` and ``abs_path ->
+  absolute``.
+- ``ActionPathList`` is deprecated and will be removed in v5.0.0. Instead use as
+  type ``List[<path_type>]`` with ``enable_path=True``.
+
+
 v4.19.0 (2022-12-27)
 --------------------
 
diff --git a/README.rst b/README.rst
index e15576d..cc0fc70 100644
--- a/README.rst
+++ b/README.rst
@@ -512,8 +512,7 @@ path could be included in a config file as relative with respect to the config
 file's location. After parsing it should be easy to access the parsed file path
 without having to consider the location of the config file. To help in these
 situations jsonargparse includes a type generator :func:`.path_type`, some
-predefined types (e.g. :class:`.Path_fr`) and the :class:`.ActionPathList`
-class.
+predefined types (e.g. :class:`.Path_fr`).
 
 For example suppose you have a directory with a configuration file
 ``app/config.yaml`` and some data ``app/data/info.db``. The contents of the yaml
@@ -581,39 +580,37 @@ using the :py:meth:`.Path.get_content` method. For the previous example would be
 
 An argument with a path type can be given ``nargs='+'`` to parse multiple paths.
 But it might also be wanted to parse a list of paths found in a plain text file
-or from stdin. For this the :class:`.ActionPathList` is used and as argument
-either the path to a file listing the paths is given or the special ``'-'``
-string for reading the list from stdin. Example:
+or from stdin. For this add the argument with type ``List[<path_type>]`` and
+``enable_path=True``. To read from stdin give the special string ``'-'``.
+Example:
 
 .. testsetup:: path_list
 
     cwd = os.getcwd()
     tmpdir = tempfile.mkdtemp(prefix='_jsonargparse_doctest_')
     os.chdir(tmpdir)
-    with open('paths.lst', 'w') as f:
-        f.write('paths.lst\n')
+    pathlib.Path('paths.lst').write_text('paths.lst\n')
 
     parser = ArgumentParser()
 
     stdin = sys.stdin
-    sys.stdin = open('paths.lst', 'r')
+    sys.stdin = StringIO('paths.lst\n')
 
 .. testcleanup:: path_list
 
-    sys.stdin.close()
     sys.stdin = stdin
     os.chdir(cwd)
     shutil.rmtree(tmpdir)
 
 .. testcode:: path_list
 
-    from jsonargparse import ActionPathList
-    parser.add_argument('--list', action=ActionPathList(mode='fr'))
-    cfg = parser.parse_args(['--list', 'paths.lst'])  # Text file with paths
-    cfg = parser.parse_args(['--list', '-'])          # List from stdin
+    from jsonargparse.typing import Path_fr
+    parser.add_argument('--list', type=List[Path_fr], enable_path=True)
+    cfg = parser.parse_args(['--list', 'paths.lst'])  # File with list of paths
+    cfg = parser.parse_args(['--list', '-'])          # List of paths from stdin
 
-If ``nargs='+'`` is given to ``add_argument`` with :class:`.ActionPathList` then
-a single list is generated including all paths in all provided lists.
+If ``nargs='+'`` is given to ``add_argument`` with ``List[<path_type>]`` and
+``enable_path=True`` then for each argument a list of paths is generated.
 
 .. note::
 
diff --git a/jsonargparse/actions.py b/jsonargparse/actions.py
index ee64ac1..1843a1f 100644
--- a/jsonargparse/actions.py
+++ b/jsonargparse/actions.py
@@ -1,6 +1,5 @@
 """Collection of useful actions to define arguments."""
 
-import os
 import re
 import sys
 import warnings
@@ -16,7 +15,6 @@ from .loaders_dumpers import get_loader_exceptions, load_value
 from .namespace import Namespace, split_key, split_key_root
 from .optionals import FilesCompleterMethod, get_config_read_mode
 from .type_checking import ArgumentParser
-from .typing import path_type
 from .util import (
     LoggerProperty,
     NoneType,
@@ -36,7 +34,6 @@ __all__ = [
     'ActionConfigFile',
     'ActionYesNo',
     'ActionParser',
-    'ActionPathList',
 ]
 
 
@@ -749,78 +746,3 @@ class _ActionSubCommands(_SubParsersAction):
             # Handle inner subcommands
             if subparser._subparsers is not None:
                 _ActionSubCommands.handle_subcommands(subparser, cfg, env, defaults, key+'.', fail_no_subcommand=fail_no_subcommand)
-
-
-class ActionPathList(Action, FilesCompleterMethod):
-    """Action to check and store a list of file paths read from a plain text file or stream."""
-
-    def __init__(
-        self,
-        mode: Optional[str] = None,
-        rel: str = 'cwd',
-        **kwargs
-    ):
-        """Initializer for ActionPathList instance.
-
-        Args:
-            mode: The required type and access permissions among [fdrwxcuFDRWX] as a keyword argument (uppercase means not), e.g. ActionPathList(mode='fr').
-            rel: Whether relative paths are with respect to current working directory 'cwd' or the list's parent directory 'list'.
-
-        Raises:
-            ValueError: If any of the parameters (mode or rel) are invalid.
-        """
-        if mode is not None:
-            self._type = path_type(mode)
-            self._rel = rel
-            if self._rel not in {'cwd', 'list'}:
-                raise ValueError(f'rel must be either "cwd" or "list", got {self._rel}.')
-        elif '_type' not in kwargs:
-            raise ValueError('Expected mode keyword argument.')
-        else:
-            self._type = kwargs.pop('_type')
-            self._rel = kwargs.pop('_rel')
-            super().__init__(**kwargs)
-
-    def __call__(self, *args, **kwargs):
-        """Parses an argument as a PathList and if valid sets the parsed value to the corresponding key.
-
-        Raises:
-            TypeError: If the argument is not a valid PathList.
-        """
-        if len(args) == 0:
-            if 'nargs' in kwargs and kwargs['nargs'] not in {'+', 1}:
-                raise ValueError('ActionPathList only supports nargs of 1 or "+".')
-            kwargs['_type'] = self._type
-            kwargs['_rel'] = self._rel
-            return ActionPathList(**kwargs)
-        setattr(args[1], self.dest, self._check_type(args[2]))
-        return None
-
-    def _check_type(self, value, cfg=None):
-        if value == []:
-            return value
-        islist = _is_action_value_list(self)
-        if not islist and not isinstance(value, list):
-            value = [value]
-        if isinstance(value, list) and all(not isinstance(v, self._type) for v in value):
-            path_list_files = value
-            value = []
-            for path_list_file in path_list_files:
-                try:
-                    with sys.stdin if path_list_file == '-' else open(path_list_file) as f:
-                        path_list = [x.strip() for x in f.readlines()]
-                except FileNotFoundError as ex:
-                    raise TypeError(f'Problems reading path list: {path_list_file} :: {ex}') from ex
-                cwd = os.getcwd()
-                if self._rel == 'list' and path_list_file != '-':
-                    os.chdir(os.path.abspath(os.path.join(path_list_file, os.pardir)))
-                try:
-                    for num, val in enumerate(path_list):
-                        try:
-                            path_list[num] = self._type(val)
-                        except TypeError as ex:
-                            raise TypeError(f'Path number {num+1} in list {path_list_file}, {ex}') from ex
-                finally:
-                    os.chdir(cwd)
-                value += path_list
-        return value
diff --git a/jsonargparse/deprecated.py b/jsonargparse/deprecated.py
index 3a33223..1ea9b79 100644
--- a/jsonargparse/deprecated.py
+++ b/jsonargparse/deprecated.py
@@ -3,23 +3,19 @@
 import functools
 import inspect
 import os
+import sys
+from argparse import Action
 from enum import Enum
-from typing import Any, Dict, Set
+from typing import Any, Dict, Optional, Set
 
 from .namespace import Namespace
-from .optionals import (
-    get_config_read_mode,
-    import_docstring_parser,
-    set_config_read_mode,
-)
-from .typehints import ActionTypeHint
-from .typing import path_type, registered_types, restricted_number_type
-from .util import is_subclass, warning
+from .optionals import FilesCompleterMethod
 
 __all__ = [
     'ActionEnum',
     'ActionOperators',
     'ActionPath',
+    'ActionPathList',
     'set_url_support',
 ]
 
@@ -36,6 +32,7 @@ def deprecation_warning(component, message):
     show_warnings = env_var != 'off'
     all_warnings = env_var == 'all'
     if show_warnings and (component not in shown_deprecation_warnings or all_warnings):
+        from .util import warning
         if len(shown_deprecation_warnings) == 0 and not all_warnings:
             warning(
                 """
@@ -182,6 +179,7 @@ class ActionEnum:
 
     def __init__(self, **kwargs):
         if 'enum' in kwargs:
+            from .util import is_subclass
             if not is_subclass(kwargs['enum'], Enum):
                 raise ValueError('Expected enum to be an subclass of Enum.')
             self._type = kwargs['enum']
@@ -189,6 +187,7 @@ class ActionEnum:
             raise ValueError('Expected enum keyword argument.')
 
     def __call__(self, *args, **kwargs):
+        from .typehints import ActionTypeHint
         return ActionTypeHint(typehint=self._type)(**kwargs)
 
 
@@ -203,6 +202,7 @@ class ActionOperators:
         if 'expr' in kwargs:
             restrictions = [kwargs['expr']] if isinstance(kwargs['expr'], tuple) else kwargs['expr']
             register_key = (tuple(sorted(restrictions)), kwargs.get('type', int), kwargs.get('join', 'and'))
+            from .typing import registered_types, restricted_number_type
             if register_key in registered_types:
                 self._type = registered_types[register_key]
             else:
@@ -211,6 +211,7 @@ class ActionOperators:
             raise ValueError('Expected expr keyword argument.')
 
     def __call__(self, *args, **kwargs):
+        from .typehints import ActionTypeHint
         return ActionTypeHint(typehint=self._type)(**kwargs)
 
 
@@ -226,12 +227,95 @@ class ActionPath:
         mode: str,
         skip_check: bool = False,
     ):
+        from .typing import path_type
         self._type = path_type(mode, skip_check=skip_check)
 
     def __call__(self, *args, **kwargs):
+        from .typehints import ActionTypeHint
         return ActionTypeHint(typehint=self._type)(**kwargs)
 
 
+@deprecated("""
+    ActionPathList was deprecated in v4.20.0 and will be removed in v5.0.0. Instead
+    use as type ``List[<path_type>]`` with ``enable_path=True``.
+""")
+class ActionPathList(Action, FilesCompleterMethod):
+    """Action to check and store a list of file paths read from a plain text file or stream."""
+
+    def __init__(
+        self,
+        mode: Optional[str] = None,
+        rel: str = 'cwd',
+        **kwargs
+    ):
+        """Initializer for ActionPathList instance.
+
+        Args:
+            mode: The required type and access permissions among [fdrwxcuFDRWX] as a keyword argument (uppercase means not), e.g. ActionPathList(mode='fr').
+            rel: Whether relative paths are with respect to current working directory 'cwd' or the list's parent directory 'list'.
+
+        Raises:
+            ValueError: If any of the parameters (mode or rel) are invalid.
+        """
+        if mode is not None:
+            from .typing import path_type
+            self._type = path_type(mode)
+            self._rel = rel
+            if self._rel not in {'cwd', 'list'}:
+                raise ValueError(f'rel must be either "cwd" or "list", got {self._rel}.')
+        elif '_type' not in kwargs:
+            raise ValueError('Expected mode keyword argument.')
+        else:
+            self._type = kwargs.pop('_type')
+            self._rel = kwargs.pop('_rel')
+            super().__init__(**kwargs)
+
+    def __call__(self, *args, **kwargs):
+        """Parses an argument as a PathList and if valid sets the parsed value to the corresponding key.
+
+        Raises:
+            TypeError: If the argument is not a valid PathList.
+        """
+        if len(args) == 0:
+            if 'nargs' in kwargs and kwargs['nargs'] not in {'+', 1}:
+                raise ValueError('ActionPathList only supports nargs of 1 or "+".')
+            kwargs['_type'] = self._type
+            kwargs['_rel'] = self._rel
+            return ActionPathList(**kwargs)
+        setattr(args[1], self.dest, self._check_type(args[2]))
+        return None
+
+    def _check_type(self, value, cfg=None):
+        if value == []:
+            return value
+        from .actions import _is_action_value_list
+        islist = _is_action_value_list(self)
+        if not islist and not isinstance(value, list):
+            value = [value]
+        if isinstance(value, list) and all(not isinstance(v, self._type) for v in value):
+            path_list_files = value
+            value = []
+            for path_list_file in path_list_files:
+                try:
+                    with sys.stdin if path_list_file == '-' else open(path_list_file) as f:
+                        path_list = [x.strip() for x in f.readlines()]
+                except FileNotFoundError as ex:
+                    raise TypeError(f'Problems reading path list: {path_list_file} :: {ex}') from ex
+                cwd = os.getcwd()
+                if self._rel == 'list' and path_list_file != '-':
+                    os.chdir(os.path.abspath(os.path.join(path_list_file, os.pardir)))
+                try:
+                    for num, val in enumerate(path_list):
+                        try:
+                            path_list[num] = self._type(val)
+                        except TypeError as ex:
+                            raise TypeError(f'Path number {num+1} in list {path_list_file}, {ex}') from ex
+                finally:
+                    os.chdir(cwd)
+                value += path_list
+        return value
+
+
 @deprecated("""
     set_url_support was deprecated in v3.12.0 and will be removed in v5.0.0.
     Optional config read modes should now be set using function
@@ -239,6 +323,7 @@ class ActionPath:
 """)
 def set_url_support(enabled:bool):
     """Enables/disables URL support for config read mode."""
+    from .optionals import get_config_read_mode, set_config_read_mode
     set_config_read_mode(
         urls_enabled=enabled,
         fsspec_enabled=True if 's' in get_config_read_mode() else False,
@@ -274,9 +359,82 @@ env_prefix_property_none_message = """
     v5.0.0.
 """)
 def import_docstring_parse(importer):
+    from .optionals import import_docstring_parser
     return import_docstring_parser(importer).parse
 
 
+path_skip_check_message = """
+    The skip_check parameter of Path was deprecated in v4.20.0 and will be
+    removed in v5.0.0. There is no reason to use a Path type if its checks are
+    disabled. Instead use a type such as str or os.PathLike.
+"""
+
+
+def path_skip_check_deprecation():
+    deprecation_warning('Path.__init__', path_skip_check_message)
+
+
+path_immutable_attrs_message = """
+    Path objects are not meant to be mutable. To make this more explicit,
+    attributes have been renamed and changed into properties without setters.
+    Please update your code to use the new property names and don't modify path
+    attributes. The changes are: ``rel_path`` -> ``relative`` and ``abs_path``
+    -> ``absolute``, ``cwd`` no name change, ``skip_check`` will be removed.
+
+"""
+
+class PathDeprecations:
+
+    @property
+    def rel_path(self):
+        deprecation_warning('Path attr get', path_immutable_attrs_message)
+        return self._relative
+
+    @rel_path.setter
+    def rel_path(self, rel_path):
+        deprecation_warning('Path attr set', path_immutable_attrs_message)
+        self._relative = rel_path
+
+    @property
+    def abs_path(self):
+        deprecation_warning('Path attr get', path_immutable_attrs_message)
+        return self._absolute
+
+    @abs_path.setter
+    def abs_path(self, abs_path):
+        deprecation_warning('Path attr set', path_immutable_attrs_message)
+        self._absolute = abs_path
+
+    @property
+    def cwd(self):
+        return self._cwd
+
+    @cwd.setter
+    def cwd(self, cwd):
+        deprecation_warning('Path attr set', path_immutable_attrs_message)
+        self._cwd = cwd
+
+    def _deprecated_kwargs(self, kwargs):
+        from .util import get_private_kwargs
+        self._skip_check = get_private_kwargs(kwargs, skip_check=False)
+        if self._skip_check:
+            path_skip_check_deprecation()
+
+    def _repr_skip_check(self, name):
+        if self._skip_check:
+            name += '_skip_check'
+        return name
+
+    @property
+    def skip_check(self):
+        return self._skip_check
+
+    @skip_check.setter
+    def skip_check(self, skip_check):
+        deprecation_warning('Path attr set', path_immutable_attrs_message)
+        self._skip_check = skip_check
+
+
 import jsonargparse.optionals
 
 jsonargparse.optionals.import_docstring_parse = import_docstring_parse  # type: ignore
diff --git a/jsonargparse/loaders_dumpers.py b/jsonargparse/loaders_dumpers.py
index 04bc3b1..b972dad 100644
--- a/jsonargparse/loaders_dumpers.py
+++ b/jsonargparse/loaders_dumpers.py
@@ -108,7 +108,7 @@ def load_value(value: str, simple_types: bool = False, **kwargs):
         params = set(list(inspect.signature(loader).parameters)[1:])
         kwargs = {k: v for k, v in kwargs.items() if k in params}
     loaded_value = loader(value, **kwargs)
-    if not simple_types and isinstance(loaded_value, (int, float, bool)):
+    if not simple_types and isinstance(loaded_value, (int, float, bool, str)):
         loaded_value = value
     return loaded_value
 
diff --git a/jsonargparse/typehints.py b/jsonargparse/typehints.py
index a8807b3..db7c907 100644
--- a/jsonargparse/typehints.py
+++ b/jsonargparse/typehints.py
@@ -70,6 +70,7 @@ from .util import (
     iter_to_set_str,
     object_path_serializer,
     parse_value_or_config,
+    read_stdin,
     warning,
 )
 
@@ -435,6 +436,7 @@ class ActionTypeHint(Action):
                     'sub_add_kwargs': getattr(self, 'sub_add_kwargs', {}),
                     'prev_val': prev_val,
                     'append': append,
+                    'enable_path': enable_path,
                 }
                 try:
                     with change_to_path_dir(config_path):
@@ -543,13 +545,22 @@ def raise_union_unexpected_value(uniontype, val: Any, exceptions: List[Exception
     ) from exceptions[0]
 
 
-def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, prev_val=None, append=False, sub_add_kwargs=None):
-
+def adapt_typehints(
+    val,
+    typehint,
+    serialize=False,
+    instantiate_classes=False,
+    prev_val=None,
+    append=False,
+    enable_path=False,
+    sub_add_kwargs=None,
+):
     adapt_kwargs = {
         'serialize': serialize,
         'instantiate_classes': instantiate_classes,
         'prev_val': prev_val,
         'append': append,
+        'enable_path': enable_path,
         'sub_add_kwargs': sub_add_kwargs or {},
     }
     subtypehints = getattr(typehint, '__args__', None)
@@ -665,6 +676,14 @@ def adapt_typehints(val, typehint, serialize=False, instantiate_classes=False, p
             val_is_list = isinstance(val, list)
             val = prev_val + (val if val_is_list else [val])
             prev_val = prev_val + [None] * (len(val)-len(prev_val) if val_is_list else 1)
+        if enable_path and type(val) is str:
+            if val == '-':
+                val = read_stdin().splitlines()
+            else:
+                with suppress(TypeError):
+                    from .optionals import get_config_read_mode
+                    list_path = Path(val, mode=get_config_read_mode())
+                    val = list_path.get_content().splitlines()
         if isinstance(val, NestedArg) and subtypehints is not None:
             val = (prev_val[:-1] if isinstance(prev_val, list) else []) + [val]
         elif isinstance(val, Iterable) and not isinstance(val, (list, str)) and type(val) not in mapping_origin_types:
diff --git a/jsonargparse/typing.py b/jsonargparse/typing.py
index b28eac5..1faf7f8 100644
--- a/jsonargparse/typing.py
+++ b/jsonargparse/typing.py
@@ -7,7 +7,7 @@ import re
 from typing import Any, Callable, Dict, List, Optional, Pattern, Tuple, Type, Union
 
 from .optionals import final
-from .util import Path, get_import_path, import_object
+from .util import Path, get_import_path, get_private_kwargs, import_object
 
 __all__ = [
     'final',
@@ -196,14 +196,13 @@ def _is_path_type(value, type_class):
 def path_type(
     mode: str,
     docstring: Optional[str] = None,
-    skip_check: bool = False,
+    **kwargs
 ) -> type:
     """Creates or returns an already registered path type class.
 
     Args:
         mode: The required type and access permissions among [fdrwxcuFDRWX].
         docstring: Docstring for the type class.
-        skip_check: Whether to skip path checks.
 
     Returns:
         The created or retrieved type class.
@@ -211,7 +210,11 @@ def path_type(
     Path._check_mode(mode)
     name = 'Path_'+mode
     key_name = 'path '+''.join(sorted(mode))
+
+    skip_check = get_private_kwargs(kwargs, skip_check=False)
     if skip_check:
+        from .deprecated import path_skip_check_deprecation
+        path_skip_check_deprecation()
         name += '_skip_check'
         key_name += ' skip_check'
 
diff --git a/jsonargparse/util.py b/jsonargparse/util.py
index 2dfdca1..4d56a33 100644
--- a/jsonargparse/util.py
+++ b/jsonargparse/util.py
@@ -30,6 +30,7 @@ from typing import (
 )
 
 from ._common import parser_capture, parser_context
+from .deprecated import PathDeprecations
 from .loaders_dumpers import json_dump, load_value
 from .optionals import (
     fsspec_support,
@@ -145,7 +146,7 @@ def parse_value_or_config(value: Any, enable_path: bool = True, simple_types: bo
         except TypeError:
             pass
         else:
-            with change_to_path_dir(cfg_path):
+            with cfg_path.relative_path_context():
                 value = load_value(cfg_path.get_content(), simple_types=simple_types)
     if type(value) is str and value.strip() != '':
         parsed_val = load_value(value, simple_types=simple_types)
@@ -177,6 +178,18 @@ def usage_and_exit_error_handler(parser: 'ArgumentParser', message: str) -> None
         parser.exit(2)
 
 
+class CachedStdin(StringIO):
+    """Used to allow reading sys.stdin multiple times."""
+
+
+def read_stdin() -> str:
+    if not isinstance(sys.stdin, CachedStdin):
+        sys.stdin = CachedStdin(sys.stdin.read())
+    value = sys.stdin.read()
+    sys.stdin.seek(0)
+    return value
+
+
 def is_subclass(cls, class_or_tuple):
     """Extension of issubclass that supports non-class arguments."""
     try:
@@ -295,7 +308,7 @@ def change_to_path_dir(path: Optional['Path']) -> Iterator[Optional[str]]:
             path_dir = path._url_data.url_path
         else:
             scheme = ''
-            path_dir = path.abs_path
+            path_dir = path.absolute
             chdir = True
         if 'd' not in path.mode:
             path_dir = os.path.dirname(path_dir)
@@ -408,7 +421,7 @@ def parse_url(url: str) -> Optional[UrlData]:
     )
 
 
-def is_absolute(path: str) -> bool:
+def is_absolute_path(path: str) -> bool:
     if path.find('://') > 0:
         return True
     return os.path.isabs(path)
@@ -425,7 +438,7 @@ def resolve_relative_path(path: str) -> str:
     return '/'.join(resolved)
 
 
-class Path:
+class Path(PathDeprecations):
     """Stores a (possibly relative) path and the corresponding absolute path.
 
     The absolute path can be obtained without having to remember the working
@@ -442,10 +455,6 @@ class Path:
     directory does not have to exist, but should be allowed to create.
     """
 
-    rel_path: str
-    abs_path: str
-    cwd: str
-    mode: str
     _url_data: Optional[UrlData]
     _file_scheme = re.compile('^file:///?')
 
@@ -454,20 +463,20 @@ class Path:
         path: Union[str, 'Path'],
         mode: str = 'fr',
         cwd: Optional[str] = None,
-        skip_check: bool = False,
+        **kwargs,
     ):
         """Initializer for Path instance.
 
         Args:
             path: The path to check and store.
             mode: The required type and access permissions among [fdrwxcuFDRWX].
-            cwd: Working directory for relative paths. If None, then os.getcwd() is used.
-            skip_check: Whether to skip path checks.
+            cwd: Working directory for relative paths. If None, os.getcwd() is used.
 
         Raises:
             ValueError: If the provided mode is invalid.
             TypeError: If the path does not exist or does not agree with the mode.
         """
+        self._deprecated_kwargs(kwargs)
         self._check_mode(mode)
 
         is_url = False
@@ -477,20 +486,20 @@ class Path:
             is_fsspec = path.is_fsspec
             url_data = path._url_data
             cwd = path.cwd
-            abs_path = path.abs_path
-            path = path.rel_path
+            abs_path = path._absolute
+            path = path._relative
         elif isinstance(path, str):
             abs_path = os.path.expanduser(path)
             if self._file_scheme.match(abs_path):
                 abs_path = self._file_scheme.sub('' if os.name == 'nt' else '/', abs_path)
-            absolute = is_absolute(abs_path)
+            is_absolute = is_absolute_path(abs_path)
             url_data = parse_url(abs_path)
             cwd_url_data = parse_url(cwd or current_path_dir.get() or os.getcwd())
             if (
                 ('u' in mode or 's' in mode) and
-                (url_data or (cwd_url_data and not absolute))
+                (url_data or (cwd_url_data and not is_absolute))
             ):
-                if cwd_url_data and not absolute:
+                if cwd_url_data and not is_absolute:
                     abs_path = resolve_relative_path(cwd_url_data.url_path + '/' + path)
                     abs_path = cwd_url_data.scheme + abs_path
                     url_data = parse_url(abs_path)
@@ -503,19 +512,19 @@ class Path:
             else:
                 if cwd is None:
                     cwd = os.getcwd()
-                abs_path = abs_path if absolute else os.path.join(cwd, abs_path)
+                abs_path = abs_path if is_absolute else os.path.join(cwd, abs_path)
                 url_data = None
         else:
             raise TypeError('Expected path to be a string or a Path object.')
 
-        if not skip_check and is_url:
+        if not self._skip_check and is_url:
             if 'r' in mode:
                 requests = import_requests('Path with URL support')
                 try:
                     requests.head(abs_path).raise_for_status()
                 except requests.HTTPError as ex:
                     raise TypeError(f'{abs_path} HEAD not accessible :: {ex}') from ex
-        elif not skip_check and is_fsspec:
+        elif not self._skip_check and is_fsspec:
             fsspec_mode = ''.join(c for c in mode if c in {'r','w'})
             if fsspec_mode:
                 fsspec = import_fsspec('Path')
@@ -527,7 +536,7 @@ class Path:
                     raise TypeError('Path does not exist: '+abs_path) from ex
                 except PermissionError as ex:
                     raise TypeError('Path exists but no permission to access: '+abs_path) from ex
-        elif not skip_check:
+        elif not self._skip_check:
             ptype = 'Directory' if 'd' in mode else 'File'
             if 'c' in mode:
                 pdir = os.path.realpath(os.path.join(abs_path, '..'))
@@ -568,31 +577,51 @@ class Path:
             if 'X' in mode and os.access(abs_path, os.X_OK):
                 raise TypeError(ptype+' is executable: '+abs_path)
 
-        self.rel_path = path
-        self.abs_path = abs_path
-        self.cwd = cwd
-        self.mode = mode
+        self._relative = path
+        self._absolute = abs_path
+        self._cwd = cwd
+        self._mode = mode
+        self._is_url = is_url
+        self._is_fsspec = is_fsspec
         self._url_data = url_data
-        self.is_url: bool = is_url
-        self.is_fsspec: bool = is_fsspec
-        self.skip_check = skip_check
+
+    @property
+    def relative(self) -> str:
+        return self._relative
+
+    @property
+    def absolute(self) -> str:
+        return self._absolute
+
+    @property
+    def mode(self) -> str:
+        return self._mode
+
+    @property
+    def is_url(self) -> bool:
+        return self._is_url
+
+    @property
+    def is_fsspec(self) -> bool:
+        return self._is_fsspec
 
     def __str__(self):
-        return self.rel_path
+        return self._relative
 
     def __repr__(self):
-        cwd = '' if self.rel_path == self.abs_path else ', cwd='+self.cwd
-        name = 'Path_'+self.mode
-        if self.skip_check:
-            name += '_skip_check'
-        return name+'('+self.rel_path+cwd+')'
+        name = 'Path_'+self._mode
+        name = self._repr_skip_check(name)
+        cwd = ''
+        if self._relative != self._absolute:
+            cwd = ', cwd='+self._cwd
+        return f'{name}({self._relative}{cwd})'
 
     def __fspath__(self) -> str:
-        return self.abs_path
+        return self._absolute
 
     def __eq__(self, other: Any) -> bool:
         if isinstance(other, Path):
-            return self.abs_path == other.abs_path
+            return self._absolute == other._absolute
         elif isinstance(other, str):
             return str(self) == other
         return False
@@ -603,36 +632,36 @@ class Path:
         Args:
             absolute: If false returns the original path given, otherwise the corresponding absolute path.
         """
-        return self.abs_path if absolute else self.rel_path
+        return self._absolute if absolute else self._relative
 
     def get_content(self, mode: str = 'r') -> str:
         """Returns the contents of the file or the remote path."""
-        if self.is_url:
+        if self._is_url:
             assert mode == 'r'
             requests = import_requests('Path.get_content')
-            response = requests.get(self.abs_path)
+            response = requests.get(self._absolute)
             response.raise_for_status()
             return response.text
-        elif self.is_fsspec:
+        elif self._is_fsspec:
             fsspec = import_fsspec('Path.get_content')
-            with fsspec.open(self.abs_path, mode) as handle:
+            with fsspec.open(self._absolute, mode) as handle:
                 with handle as input_file:
                     return input_file.read()
         else:
-            with open(self.abs_path, mode) as input_file:
+            with open(self._absolute, mode) as input_file:
                 return input_file.read()
 
     @contextmanager
     def open(self, mode: str = 'r') -> Iterator[IO]:
         """Return an opened file object for the path."""
-        if self.is_url:
+        if self._is_url:
             yield StringIO(self.get_content())
-        elif self.is_fsspec:
+        elif self._is_fsspec:
             fsspec = import_fsspec('Path.open')
-            with fsspec.open(self.abs_path, mode) as handle:
+            with fsspec.open(self._absolute, mode) as handle:
                 yield handle
         else:
-            with open(self.abs_path, mode) as handle:
+            with open(self._absolute, mode) as handle:
                 yield handle
 
     @contextmanager
diff --git a/sphinx/conf.py b/sphinx/conf.py
index 9fdab54..34fb9c6 100644
--- a/sphinx/conf.py
+++ b/sphinx/conf.py
@@ -70,11 +70,13 @@ doctest.OutputChecker = CustomOutputChecker
 
 doctest_global_setup = '''
 import os
+import pathlib
 import shutil
 import sys
 import tempfile
 from calendar import Calendar
 from dataclasses import dataclass
+from io import StringIO
 from typing import Callable, Iterable, List
 import jsonargparse_tests
 from jsonargparse import *


Test patch:
diff --git a/jsonargparse_tests/test_actions.py b/jsonargparse_tests/test_actions.py
index e30d31f..c90b01b 100755
--- a/jsonargparse_tests/test_actions.py
+++ b/jsonargparse_tests/test_actions.py
@@ -2,14 +2,12 @@
 
 import json
 import os
-import pathlib
 import unittest
 from io import StringIO
 
 from jsonargparse import (
     ActionConfigFile,
     ActionParser,
-    ActionPathList,
     ActionYesNo,
     ArgumentParser,
     ParserError,
@@ -74,61 +72,6 @@ class SimpleActionsTests(unittest.TestCase):
         self.assertRaises(ParserError, lambda: parser.parse_args(['--val', '1']))
 
 
-class ActionPathTests(TempDirTestCase):
-
-    def test_ActionPathList(self):
-        tmpdir = os.path.join(self.tmpdir, 'subdir')
-        os.mkdir(tmpdir)
-        pathlib.Path(os.path.join(tmpdir, 'file1')).touch()
-        pathlib.Path(os.path.join(tmpdir, 'file2')).touch()
-        pathlib.Path(os.path.join(tmpdir, 'file3')).touch()
-        pathlib.Path(os.path.join(tmpdir, 'file4')).touch()
-        pathlib.Path(os.path.join(tmpdir, 'file5')).touch()
-        list_file = os.path.join(tmpdir, 'files.lst')
-        list_file2 = os.path.join(tmpdir, 'files2.lst')
-        list_file3 = os.path.join(tmpdir, 'files3.lst')
-        list_file4 = os.path.join(tmpdir, 'files4.lst')
-        with open(list_file, 'w') as output_file:
-            output_file.write('file1\nfile2\nfile3\nfile4\n')
-        with open(list_file2, 'w') as output_file:
-            output_file.write('file5\n')
-        pathlib.Path(list_file3).touch()
-        with open(list_file4, 'w') as output_file:
-            output_file.write('file1\nfile2\nfile6\n')
-
-        parser = ArgumentParser(prog='app', error_handler=None)
-        parser.add_argument('--list',
-            nargs='+',
-            action=ActionPathList(mode='fr', rel='list'))
-        parser.add_argument('--list_cwd',
-            action=ActionPathList(mode='fr', rel='cwd'))
-
-        cfg = parser.parse_args(['--list', list_file])
-        self.assertEqual(4, len(cfg.list))
-        self.assertEqual(['file1', 'file2', 'file3', 'file4'], [str(x) for x in cfg.list])
-
-        cfg = parser.parse_args(['--list', list_file, list_file2])
-        self.assertEqual(5, len(cfg.list))
-        self.assertEqual(['file1', 'file2', 'file3', 'file4', 'file5'], [str(x) for x in cfg.list])
-
-        self.assertEqual(0, len(parser.parse_args(['--list', list_file3]).list))
-
-        cwd = os.getcwd()
-        os.chdir(tmpdir)
-        cfg = parser.parse_args(['--list_cwd', list_file])
-        self.assertEqual(4, len(cfg.list_cwd))
-        self.assertEqual(['file1', 'file2', 'file3', 'file4'], [str(x) for x in cfg.list_cwd])
-        os.chdir(cwd)
-
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--list']))
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--list', list_file4]))
-        self.assertRaises(ParserError, lambda: parser.parse_args(['--list', 'no-such-file']))
-
-        self.assertRaises(ValueError, lambda: parser.add_argument('--op1', action=ActionPathList))
-        self.assertRaises(ValueError, lambda: parser.add_argument('--op2', action=ActionPathList(mode='fr'), nargs='*'))
-        self.assertRaises(ValueError, lambda: parser.add_argument('--op3', action=ActionPathList(mode='fr', rel='.')))
-
-
 class ActionParserTests(TempDirTestCase):
 
     def test_ActionParser(self):
diff --git a/jsonargparse_tests/test_deprecated.py b/jsonargparse_tests/test_deprecated.py
index eac2d95..8c13931 100755
--- a/jsonargparse_tests/test_deprecated.py
+++ b/jsonargparse_tests/test_deprecated.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 
 import os
+import pathlib
 import unittest
 from calendar import Calendar
 from enum import Enum
@@ -20,6 +21,7 @@ from jsonargparse.deprecated import (
     ActionEnum,
     ActionOperators,
     ActionPath,
+    ActionPathList,
     deprecation_warning,
     import_docstring_parse,
     shown_deprecation_warnings,
@@ -275,7 +277,9 @@ class DeprecatedTempDirTests(TempDirTestCase):
 
     def test_ActionPath_skip_check(self):
         parser = ArgumentParser(error_handler=None)
-        parser.add_argument('--file', action=ActionPath(mode='fr', skip_check=True))
+        with catch_warnings(record=True) as w:
+            parser.add_argument('--file', action=ActionPath(mode='fr', skip_check=True))
+            self.assertIn('skip_check parameter of Path was deprecated', str(w[-1].message))
         cfg = parser.parse_args(['--file=not-exist'])
         self.assertIsInstance(cfg.file, Path)
         self.assertEqual(str(cfg.file), 'not-exist')
@@ -303,5 +307,75 @@ class DeprecatedTempDirTests(TempDirTestCase):
         self.assertIsNotNone(parser.parse_args(['2', 'file']).path)
 
 
+    def test_Path_attr_set(self):
+        path = Path('file', 'fc')
+        with catch_warnings(record=True) as w:
+            path.rel_path = 'file'
+            path.abs_path = os.path.join(self.tmpdir, 'file')
+            path.skip_check = False
+            path.cwd = self.tmpdir
+            self.assertIn('Path objects are not meant to be mutable', str(w[-1].message))
+        with catch_warnings(record=True) as w:
+            self.assertEqual(path.rel_path, 'file')
+            self.assertEqual(path.abs_path, os.path.join(self.tmpdir, 'file'))
+            self.assertEqual(path.skip_check, False)
+            self.assertIn('Path objects are not meant to be mutable', str(w[-1].message))
+
+
+    def test_ActionPathList(self):
+        tmpdir = os.path.join(self.tmpdir, 'subdir')
+        os.mkdir(tmpdir)
+        pathlib.Path(os.path.join(tmpdir, 'file1')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file2')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file3')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file4')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file5')).touch()
+        list_file = os.path.join(tmpdir, 'files.lst')
+        list_file2 = os.path.join(tmpdir, 'files2.lst')
+        list_file3 = os.path.join(tmpdir, 'files3.lst')
+        list_file4 = os.path.join(tmpdir, 'files4.lst')
+        with open(list_file, 'w') as output_file:
+            output_file.write('file1\nfile2\nfile3\nfile4\n')
+        with open(list_file2, 'w') as output_file:
+            output_file.write('file5\n')
+        pathlib.Path(list_file3).touch()
+        with open(list_file4, 'w') as output_file:
+            output_file.write('file1\nfile2\nfile6\n')
+
+        parser = ArgumentParser(prog='app', error_handler=None)
+        with catch_warnings(record=True) as w:
+            parser.add_argument('--list',
+                nargs='+',
+                action=ActionPathList(mode='fr', rel='list'))
+            self.assertIn('ActionPathList was deprecated', str(w[-1].message))
+        parser.add_argument('--list_cwd',
+            action=ActionPathList(mode='fr', rel='cwd'))
+
+        cfg = parser.parse_args(['--list', list_file])
+        self.assertEqual(4, len(cfg.list))
+        self.assertEqual(['file1', 'file2', 'file3', 'file4'], [str(x) for x in cfg.list])
+
+        cfg = parser.parse_args(['--list', list_file, list_file2])
+        self.assertEqual(5, len(cfg.list))
+        self.assertEqual(['file1', 'file2', 'file3', 'file4', 'file5'], [str(x) for x in cfg.list])
+
+        self.assertEqual(0, len(parser.parse_args(['--list', list_file3]).list))
+
+        cwd = os.getcwd()
+        os.chdir(tmpdir)
+        cfg = parser.parse_args(['--list_cwd', list_file])
+        self.assertEqual(4, len(cfg.list_cwd))
+        self.assertEqual(['file1', 'file2', 'file3', 'file4'], [str(x) for x in cfg.list_cwd])
+        os.chdir(cwd)
+
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--list']))
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--list', list_file4]))
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--list', 'no-such-file']))
+
+        self.assertRaises(ValueError, lambda: parser.add_argument('--op1', action=ActionPathList))
+        self.assertRaises(ValueError, lambda: parser.add_argument('--op2', action=ActionPathList(mode='fr'), nargs='*'))
+        self.assertRaises(ValueError, lambda: parser.add_argument('--op3', action=ActionPathList(mode='fr', rel='.')))
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index 3df0355..19f7c09 100755
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -1457,16 +1457,79 @@ class TypeHintsTmpdirTests(TempDirTestCase):
         self.assertRaises(ParserError, lambda: parser.parse_args(['--data=does-not-exist.yaml']))
 
 
+    def test_list_path_with_enable_path(self):
+        tmpdir = os.path.join(self.tmpdir, 'subdir')
+        os.mkdir(tmpdir)
+        pathlib.Path(os.path.join(tmpdir, 'file1')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file2')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file3')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file4')).touch()
+        pathlib.Path(os.path.join(tmpdir, 'file5')).touch()
+        list_file = os.path.join(tmpdir, 'files.lst')
+        list_file2 = os.path.join(tmpdir, 'files2.lst')
+        list_file3 = os.path.join(tmpdir, 'files3.lst')
+        list_file4 = os.path.join(tmpdir, 'files4.lst')
+        with open(list_file, 'w') as output_file:
+            output_file.write('file1\nfile2\nfile3\nfile4\n')
+        with open(list_file2, 'w') as output_file:
+            output_file.write('file5\n')
+        pathlib.Path(list_file3).touch()
+        with open(list_file4, 'w') as output_file:
+            output_file.write('file1\nfile2\nfile6\n')
+
+        parser = ArgumentParser(error_handler=None)
+        parser.add_argument(
+            '--lists',
+            nargs='+',
+            type=List[Path_fr],
+            enable_path=True,
+        )
+        parser.add_argument(
+            '--list',
+            type=List[Path_fr],
+            enable_path=True,
+        )
+
+        cfg = parser.parse_args(['--list', list_file])
+        self.assertTrue(all(isinstance(x, Path_fr) for x in cfg.list))
+        self.assertEqual(['file1', 'file2', 'file3', 'file4'], [str(x) for x in cfg.list])
+
+        with unittest.mock.patch('sys.stdin', StringIO('file1\nfile2\n')):
+            with self.assertRaises(ParserError):
+                parser.parse_args(['--list', '-'])
+            with Path_drw('subdir').relative_path_context():
+                cfg = parser.parse_args(['--list', '-'])
+        self.assertTrue(all(isinstance(x, Path_fr) for x in cfg.list))
+        self.assertEqual(['file1', 'file2'], [str(x) for x in cfg.list])
+
+        cfg = parser.parse_args(['--lists', list_file])
+        self.assertEqual(1, len(cfg.lists))
+        self.assertTrue(all(isinstance(x, Path_fr) for x in cfg.lists[0]))
+        self.assertEqual(['file1', 'file2', 'file3', 'file4'], [str(x) for x in cfg.lists[0]])
+
+        cfg = parser.parse_args(['--lists', list_file, list_file2])
+        self.assertEqual(2, len(cfg.lists))
+        self.assertEqual(['file1', 'file2', 'file3', 'file4'], [str(x) for x in cfg.lists[0]])
+        self.assertEqual(['file5'], [str(x) for x in cfg.lists[1]])
+
+        cfg = parser.parse_args(['--lists', list_file3])
+        self.assertEqual([[]], cfg.lists)
+
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--lists']))
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--lists', list_file4]))
+        self.assertRaises(ParserError, lambda: parser.parse_args(['--lists', 'no-such-file']))
+
+
     def test_default_path_unregistered_type(self):
         parser = ArgumentParser()
         parser.add_argument('--path',
-                            type=path_type('drw', skip_check=True),
-                            default=Path('test', mode='drw', skip_check=True))
+                            type=path_type('dcc'),
+                            default=Path('test', mode='dcc'))
         cfg = parser.parse_args([])
         self.assertEqual('path: test\n', parser.dump(cfg))
         out = StringIO()
         parser.print_help(out)
-        self.assertIn('(type: Path_drw_skip_check, default: test)', out.getvalue())
+        self.assertIn('(type: Path_dcc, default: test)', out.getvalue())
 
 
     def test_path_like_within_subclass(self):
diff --git a/jsonargparse_tests/test_util.py b/jsonargparse_tests/test_util.py
index 4441432..636f2d2 100755
--- a/jsonargparse_tests/test_util.py
+++ b/jsonargparse_tests/test_util.py
@@ -68,8 +68,8 @@ class PathTests(TempDirTestCase):
         path1 = Path(self.file_rw, 'frw')
         path2 = Path(path1)
         self.assertEqual(path1.cwd, path2.cwd)
-        self.assertEqual(path1.abs_path, path2.abs_path)
-        self.assertEqual(path1.rel_path, path2.rel_path)
+        self.assertEqual(path1.absolute, path2.absolute)
+        self.assertEqual(path1.relative, path2.relative)
         self.assertEqual(path1.is_url, path2.is_url)
         self.assertRaises(TypeError, lambda: Path(True))
         self.assertRaises(ValueError, lambda: Path(self.file_rw, '-'))