Repository: cpplint/cpplint
Pull request #317: Refactor tests and metadata
Author: login: aaronliu0130
Created: 2025-03-04T00:06:45Z | Merged: 2025-03-11T22:29:07Z

> [!NOTE]
> We should remove the pylint warning suppression on getopt when the next release of pylint comes out due to getopt becoming deprecated for like 3 months

Reduce duplication of sample tests through parameterization, making pytest (and unittest) list the individual now-filterable tests (closes #260)
* Replace duplicative testing of headers in boost's exclude.def with something where the --exclude parameter actually has an effect
* Took the opportunity to lint CLI tests and migrate .popen() to .run()

Update fields of pyproject.toml
* merge pylint config
* make version dynamic
* better list of maintainers and major authors
* remove "test" extra which now just duplicates "dev"
* remove weird duplicated "testing" extra
* remove setuptools—a build-system.requires—from optional requirements
* remove unneeded pytest alias
* more and better keywords (this is PyPI, we don't need a "Python" keyword
* shorter desc

Linked issue #260: Split clitests
Currently, the silly-sample and boost samples include a lot of tests, yet they are independently bundled as one test. Splittin gthem would be a lot more useful to see which part failed.

The only approach I can think of is splitting the test function into multiple manual calls to checking a def. @jayvdb Do you have any other ideas?

Diff:
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 220215c..0000000
--- a/.pylintrc
+++ /dev/null
@@ -1,49 +0,0 @@
-[BASIC]
-include-naming-hint=yes
-method-rgx=[A-Z_][A-Za-z0-9]{2,49}$|__init__|__str__|__contains__
-function-rgx=[A-Z_][A-Za-z0-9]{2,49}$|main
-const-rgx=[a-zA-Z_][A-Za-z0-9_]{2,49}$
-variable-rgx=[a-z_][a-z0-9_]{0,49}$
-argument-rgx=[a-z_][a-z0-9_]{0,49}$
-class-rgx=[A-Z_][a-zA-Z0-9]+$|basestring|unicode|long|xrange
-
-[MESSAGES CONTROL]
-disable=
-  global-statement,
-  multiple-statements,
-  missing-docstring,
-  no-else-return,
-  no-self-use,
-  consider-merging-isinstance,
-  consider-using-min-builtin,
-  bad-continuation,
-  fixme,
-  bad-option-value,
-  anomalous-unicode-escape-in-string,
-  unused-argument,
-  useless-object-inheritance,
-  consider-using-dict-comprehension,
-  consider-using-in,
-  unnecessary-pass,
-  too-many-positional-arguments
-
-[REPORTS]
-output-format=colorized
-reports=no
-score=no
-
-[FORMAT]
-indent-string='  '
-indent-after-paren=4
-max-module-lines=10000
-
-[DESIGN]
-max-locals=25
-max-line-length=100
-max-attributes=10
-max-branches=30
-max-args=20
-max-statements=75
-max-returns=10
-min-public-methods=0
-max-bool-expr=10
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 4ff2e0f..fe310c8 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -7,7 +7,7 @@ Thanks for your interest in contributing to cpplint.
 Any kinds of contributions are welcome: Bug reports, Documentation, Patches. However, here are some contributions you probably shouldn't make:
 
 * Drastic reorganization
-  * Making the code conform to Google's Python style guidelines
+   * Making the code conform to Google's Python style guidelines
 * Features that could be regarded as a security vulnerability
 
 If you need some ideas, you may check out some of the tasks in our `issue tracker <https://github.com/cpplint/cpplint/issues>`_.
@@ -22,9 +22,9 @@ For many tasks, it is okay to just develop using a single installed python versi
    1. (Optional) Install `pyenv <https://github.com/pyenv/pyenv-installer>`_ to manage python versions
    2. (Optional) Using pyenv, install the python versions used in testing::
 
-        pyenv install 3.12.6
+        pyenv install 3.<version>
         # ...
-        pyenv local 3.12.6 ...
+        pyenv local 3.<version> ...
 
 It may be okay to run and test python against locally installed libraries, but if you need to have a consistent build, it is recommended to manage your environment using virtualenv: `virtualenv <https://virtualenv.pypa.io/en/latest/>`_, `virtualenvwrapper <https://pypi.org/project/virtualenvwrapper/>`_::
 
diff --git a/cpplint_clitest.py b/cpplint_clitest.py
index d49d08c..3da296c 100755
--- a/cpplint_clitest.py
+++ b/cpplint_clitest.py
@@ -39,50 +39,51 @@
 import tempfile
 import unittest
 
+from parameterized import parameterized
 from pytest import mark
 from testfixtures import compare
 
 BASE_CMD = sys.executable + ' ' + os.path.abspath('./cpplint.py ')
 
-def RunShellCommand(cmd: str, args: str, cwd='.'):
-    """
-    executes a command
-    :param cmd: A string to execute.
-    :param cwd: from which folder to run.
-    """
 
-    stdout_target = subprocess.PIPE
-    stderr_target = subprocess.PIPE
+def run_shell_command(cmd: str, args: str, cwd='.'):
+    """Executes a command
 
-    proc = subprocess.Popen(cmd + ' ' + args,
-                            shell=True,
-                            cwd=cwd,
-                            stdout=stdout_target,
-                            stderr=stderr_target)
-    out, err = proc.communicate()
+    Args:
+        cmd: A string to execute.
+        args: A string with arguments to the command.
+        cwd: from which folder to run.
+    """
+    cmd, args = cmd.split(), args.split()
+    proc = subprocess.run(cmd + args, cwd=cwd, capture_output=True)
+    out, err = proc.stdout, proc.stderr
 
     # Make output system-agnostic, aka support Windows
     if os.sep == '\\':
-        # TODO: Support scenario with multiple folder inputs
-        win_path = (os.path.dirname(args.split(' ')[-1]) + '\\').encode()
+        # TODO: Support scenario with multiple input names
+        # We currently only support the last arguments as the input name
+        # to prevent accidentally replacing sed tests.
+        # Fixing would likely need coding an internal "replace slashes" option for cpplint itself.
+        win_path = (os.path.dirname(args[-1]) + '\\').encode()
         good_path = win_path.replace(b'\\', b'/')
         out, err = out.replace(win_path, good_path), err.replace(win_path, good_path)
     if os.linesep == '\r\n':
         out, err = out.replace(b'\r\n', b'\n'), err.replace(b'\r\n', b'\n')
 
     # print(err) # to get the output at time of test
-    return (proc.returncode, out, err)
+    return proc.returncode, out, err
 
 
 class UsageTest(unittest.TestCase):
 
     def testHelp(self):
-        (status, out, err) = RunShellCommand(BASE_CMD, '--help')
+        (status, out, err) = run_shell_command(BASE_CMD, '--help')
         self.assertEqual(0, status)
         self.assertEqual(b'', out)
         self.assertTrue(err.startswith(b'\nSyntax: cpplint'))
 
-class TemporaryFolderClassSetup(object):
+
+class TemporaryFolderClassSetup(unittest.TestCase):
     """
     Regression tests: The test starts a filetreewalker scanning for files name *.def
     Such files are expected to have as first line the argument
@@ -108,7 +109,7 @@ def setUpClass(cls):
 
     @classmethod
     def tearDownClass(cls):
-        if (cls._root):
+        if cls._root:
             # pass
             shutil.rmtree(cls._root)
 
@@ -121,39 +122,39 @@ def get_extra_command_args(self, cwd):
         """Override in subclass to add arguments to command"""
         return ''
 
-    def checkAllInFolder(self, foldername, expectedDefs):
+    def check_all_in_folder(self, folder_name, expected_defs):
         # uncomment to show complete diff
         # self.maxDiff = None
         count = 0
-        for dirpath, _, fnames in os.walk(foldername):
+        for dirpath, _, fnames in os.walk(folder_name):
             for f in fnames:
                 if f.endswith('.def'):
                     count += 1
-                    self._checkDef(os.path.join(dirpath, f))
-        self.assertEqual(count, expectedDefs)
+                    self.check_def(os.path.join(dirpath, f))
+        self.assertEqual(count, expected_defs)
 
-    def _checkDef(self, path):
+    def check_def(self, path):
         """runs command and compares to expected output from def file"""
         # self.maxDiff = None # to see full diff
-        with open(path, 'rb') as filehandle:
-            datalines = filehandle.readlines()
-            stdoutLines = int(datalines[2])
-            filenames = datalines[0].decode('utf8').strip()
+        with open(path, 'rb') as file_handle:
+            data = file_handle.readlines()
+            stdout_lines = int(data[2])
+            filenames = data[0].decode('utf8').strip()
             args, _, filenames = filenames.rpartition(" ")
             if '*' in filenames:
                 rel_cwd = os.path.dirname(path)
                 filenames = ' '.join(
-                    filename[len(rel_cwd)+1:]
+                    filename[len(rel_cwd) + 1:]
                     for filename in glob.glob(rel_cwd + '/' + filenames)
                 )
             args += ' ' + filenames
-            self._runAndCheck(path,
-                              args,
-                              int(datalines[1]),
-                              [line.decode('utf8').strip() for line in datalines[3:3 + stdoutLines]],
-                              [line.decode('utf8').strip() for line in datalines[3 + stdoutLines:]])
+            self._run_and_compare(path, args, int(data[1]),
+                                  [line.decode('utf8').strip()
+                                   for line in data[3:3 + stdout_lines]],
+                                  [line.decode('utf8').strip()
+                                   for line in data[3 + stdout_lines:]])
 
-    def _runAndCheck(
+    def _run_and_compare(
             self,
             definition_file,
             args,
@@ -165,8 +166,8 @@ def _runAndCheck(
         cmd = BASE_CMD + self.get_extra_command_args(rel_cwd)
         cwd = os.path.join(self._root, rel_cwd)
         # command to reproduce, do not forget first two lines have special meaning
-        print("\ncd " + cwd + " && " + cmd + ' '  + args + " 2> <filename>")
-        (status, out, err) = RunShellCommand(cmd, args, cwd)
+        print("\ncd " + cwd + " && " + cmd + ' ' + args + " 2> <filename>")
+        (status, out, err) = run_shell_command(cmd, args, cwd)
         self.assertEqual(expected_status, status, 'bad command status %s' % status)
         prefix = 'Failed check in %s comparing to %s for command: %s' % (cwd, definition_file, cmd)
         compare('\n'.join(expected_err), err.decode('utf8'), prefix=prefix, show_whitespace=True)
@@ -177,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
+                           for folder in ['chromium', 'vlc', 'silly',
+                                          'boost', 'protobuf', 'codelite', 'v8']
+                           for case in os.listdir(f'./samples/{folder}-sample')
+                           if case.endswith('.def')],
+                          name_func=_test_name_func)
     @mark.timeout(180)
-    def testProtobufSample(self):
-        self.checkAllInFolder('./samples/protobuf-sample', 1)
-
-    def testCodeliteSample(self):
-        self.checkAllInFolder('./samples/codelite-sample', 1)
+    def testSamples(self, folder, case):
+        self.check_def(os.path.join(f'./samples/{folder}-sample', case + '.def'))
 
-    def testV8Sample(self):
-        self.checkAllInFolder('./samples/v8-sample', 1)
 
 class GitRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder with .git file"""
@@ -210,7 +204,8 @@ def prepare_directory(cls, root):
             pass
 
     def testCodeliteSample(self):
-        self.checkAllInFolder('./samples/codelite-sample', 1)
+        self.check_all_in_folder('./samples/codelite-sample', 1)
+
 
 class MercurialRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder with .hg file"""
@@ -221,7 +216,8 @@ def prepare_directory(cls, root):
             pass
 
     def testCodeliteSample(self):
-        self.checkAllInFolder('./samples/codelite-sample', 1)
+        self.check_all_in_folder('./samples/codelite-sample', 1)
+
 
 class SvnRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder with .svn file"""
@@ -232,7 +228,8 @@ def prepare_directory(cls, root):
             pass
 
     def testCodeliteSample(self):
-        self.checkAllInFolder('./samples/codelite-sample', 1)
+        self.check_all_in_folder('./samples/codelite-sample', 1)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pyproject.toml b/pyproject.toml
index a194c2b..ce149bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,12 +4,21 @@ requires = [ "setuptools>=61.2" ]
 
 [project]
 name = "cpplint"
-version = "2.0.0"
-description = "Automated checker to ensure C++ files follow Google's style guide"
+description = "Check C++ files configurably against Google's style guide"
 readme = "README.rst"
-keywords = [ "c++", "lint", "python" ]
+keywords = [ "c++", "cpp", "google style", "lint" ]
 license = { text = "BSD-3-Clause" }
-maintainers = [ { name = "cpplint Developers" } ]
+maintainers = [
+  { name = "Aaron Liu", email = "aaronliu0130@gmail.com" },
+  { name = "Christian Clauss", email = "cclauss@me.com" },
+  { name = "John Vandenberg", email = "jayvdb@gmail.com" },
+]
+authors = [
+  { name = "Google Inc." },
+  { name = "Thibault Kruse" },
+  { name = "Andrew Davis", email = "theandrewdavis@gmail.com" },
+  { name = "cpplint developers" },
+]
 requires-python = ">=3.8"
 classifiers = [
   "Development Status :: 5 - Production/Stable",
@@ -28,6 +37,7 @@ classifiers = [
   "Programming Language :: Python :: 3.13",
   "Topic :: Software Development :: Quality Assurance",
 ]
+dynamic = [ "version" ]
 dependencies = [  ]
 
 optional-dependencies.dev = [
@@ -36,33 +46,16 @@ optional-dependencies.dev = [
   "pytest",
   "pytest-cov",
   "pytest-timeout",
-  "setuptools",
-  "testfixtures",
-  "tox<5",
-]
-optional-dependencies.test = [
-  "parameterized",
-  "pytest",
-  "pytest-cov",
-  "pytest-timeout",
   "testfixtures",
   "tox<5",
 ]
-optional-dependencies.testing = [
-  "parameterized",
-  "pytest",
-  "pytest-cov",
-  "pytest-timeout",
-  "testfixtures",
-  "tox<5",
-]
-urls.Download = "https://github.com/cpplint/cpplint"
 urls.Homepage = "https://github.com/cpplint/cpplint"
 scripts.cpplint = "cpplint:main"
 
 [tool.setuptools]
 py-modules = [ "cpplint" ]
 include-package-data = false
+dynamic.version = { attr = "cpplint.__VERSION__" }
 
 [tool.ruff]
 target-version = "py38"
@@ -143,6 +136,57 @@ lint.per-file-ignores."cpplint.py" = [ "ICN001", "PERF401" ]
 lint.per-file-ignores."cpplint_unittest.py" = [ "FLY002" ]
 lint.mccabe.max-complexity = 29
 
+[tool.pylint.basic]
+include-naming-hint = true
+method-rgx = "[A-Z_][A-Za-z0-9]{2,49}$|__init__|__str__|__contains__"
+function-rgx = "[A-Z_][A-Za-z0-9]{2,49}$|main"
+const-rgx = "[a-zA-Z_][A-Za-z0-9_]{2,49}$"
+variable-rgx = "[a-z_][a-z0-9_]{0,49}$"
+argument-rgx = "[a-z_][a-z0-9_]{0,49}$"
+class-rgx = "[A-Z_][a-zA-Z0-9]+$"
+
+[tool.pylint.messages-control]
+disable = [
+  "global-statement",
+  "multiple-statements",
+  "missing-docstring",
+  "no-else-return",
+  "no-self-use",
+  "consider-merging-isinstance",
+  "consider-using-min-builtin",
+  "bad-continuation",
+  "fixme",
+  "bad-option-value",
+  "anomalous-unicode-escape-in-string",
+  "unused-argument",
+  "useless-object-inheritance",
+  "consider-using-dict-comprehension",
+  "consider-using-in",
+  "unnecessary-pass",
+  "too-many-positional-arguments",
+]
+
+[tool.pylint.reports]
+output-format = "colorized"
+reports = false
+score = false
+
+[tool.pylint.format]
+indent-string = '  '
+indent-after-paren = 4
+max-module-lines = 10000
+
+[tool.pylint.design]
+max-locals = 25
+max-line-length = 100
+max-attributes = 10
+max-branches = 30
+max-args = 20
+max-statements = 75
+max-returns = 10
+min-public-methods = 0
+max-bool-expr = 10
+
 [tool.pytest.ini_options]
 python_files = [ "*test.py" ]
 testpaths = [ "." ]
@@ -150,6 +194,3 @@ required_plugins = [ "pytest-cov", "pytest-timeout" ]
 timeout = 60
 # fail if coverage is under 90%
 addopts = "--color=yes --cov-fail-under=90 --cov=cpplint"
-
-[tool.aliases]
-test = "pytest"


Reviews:
user: login: cclauss
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-04T08:36:35Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T23:53:22Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T00:04:39Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T03:03:54Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T03:19:00Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T21:55:47Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T22:01:31Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T22:19:24Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T22:21:16Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T06:12:37Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T17:05:31Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T17:07:14Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T06:49:27Z
user: login: cclauss
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-10T07:04:11Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T10:35:27Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T10:54:28Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T11:39:04Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T12:53:11Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T12:53:52Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T15:57:37Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T15:58:54Z
user: login: cclauss
body: I approve this after:
1. The git conflicts are cleared, and
2. All GitHub Actions tests pass.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-11T22:04:29Z
user: login: cclauss
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T22:10:58Z
user: login: aaronliu0130
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T22:12:23Z

Review comments:
diff_hunk: @@ -14,7 +14,7 @@ jobs:
     strategy:
       matrix:
         # Python 3.8 is the last non-EOL version. Also adapt tox.ini
-        python-version: ['3.8', 'pypy3.10', '3.12']
+        python-version: ['3.8', 'pypy3.10', '3.x']
path: .github/workflows/ci.yml
user: login: cclauss
body: I prefer to push users to not build on tools that no longer receive security updates.
If they want to use EOL versions of Python, pip will just install an earlier version of cpplint.

created_at: 2025-03-04T08:34:06Z
author_association: MEMBER
diff_hunk: @@ -1,10 +1,6 @@
-[aliases]
-test = pytest
-
-[tool:pytest]
+[pytest]
path: .pytest.ini
user: login: cclauss
body: I would put this in `pyproject.toml` to have fewer config files to manage/modify.
created_at: 2025-03-04T08:34:15Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,49 @@
 [build-system]
-build-backend = "setuptools.build_meta:__legacy__"
+build-backend = "setuptools.build_meta"
 requires = [
     "setuptools",
     "wheel",
 ]
+
+[project]
+name = "cpplint"
+dynamic = ["version", "optional-dependencies"]
+maintainers = [
+    { name = "Aaron Liu", email = "aaronliu0130@gmail.com" },
+    { name = "John Vandenberg", email = "jayvdb@gmail.com" }
+]
+authors = [
+    { name = "Google Inc."},
+    { name = "Thibault Kruse"},
+    { name = "Andrew Davis", email = "theandrewdavis@gmail.com" }
+]
+description="Check C++ files configurably against Google's style guide"
+readme = "README.rst"
+requires-python = ">=3.8"
path: pyproject.toml
user: login: cclauss
body: ```suggestion
requires-python = ">=3.9"
```
created_at: 2025-03-04T08:34:41Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,49 @@
 [build-system]
-build-backend = "setuptools.build_meta:__legacy__"
+build-backend = "setuptools.build_meta"
 requires = [
     "setuptools",
     "wheel",
 ]
+
+[project]
+name = "cpplint"
+dynamic = ["version", "optional-dependencies"]
+maintainers = [
+    { name = "Aaron Liu", email = "aaronliu0130@gmail.com" },
+    { name = "John Vandenberg", email = "jayvdb@gmail.com" }
+]
+authors = [
+    { name = "Google Inc."},
+    { name = "Thibault Kruse"},
+    { name = "Andrew Davis", email = "theandrewdavis@gmail.com" }
+]
+description="Check C++ files configurably against Google's style guide"
+readme = "README.rst"
+requires-python = ">=3.8"
+keywords = ["lint", "cpp", "c++", "google style"]
+license = { text = "BSD-3-Clause" }
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: BSD License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.8",
path: pyproject.toml
user: login: cclauss
body: ```suggestion
```
created_at: 2025-03-04T08:34:46Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,49 @@
 [build-system]
-build-backend = "setuptools.build_meta:__legacy__"
+build-backend = "setuptools.build_meta"
 requires = [
     "setuptools",
     "wheel",
 ]
+
+[project]
+name = "cpplint"
+dynamic = ["version", "optional-dependencies"]
+maintainers = [
+    { name = "Aaron Liu", email = "aaronliu0130@gmail.com" },
+    { name = "John Vandenberg", email = "jayvdb@gmail.com" }
+]
+authors = [
+    { name = "Google Inc."},
+    { name = "Thibault Kruse"},
+    { name = "Andrew Davis", email = "theandrewdavis@gmail.com" }
+]
+description="Check C++ files configurably against Google's style guide"
+readme = "README.rst"
+requires-python = ">=3.8"
+keywords = ["lint", "cpp", "c++", "google style"]
+license = { text = "BSD-3-Clause" }
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: BSD License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
path: pyproject.toml
user: login: cclauss
body: Let's not promise Py3.14 support before it ships or at least release candidate.
```suggestion
```
created_at: 2025-03-04T08:34:52Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,49 @@
 [build-system]
-build-backend = "setuptools.build_meta:__legacy__"
+build-backend = "setuptools.build_meta"
 requires = [
     "setuptools",
     "wheel",
 ]
+
+[project]
+name = "cpplint"
+dynamic = ["version", "optional-dependencies"]
+maintainers = [
+    { name = "Aaron Liu", email = "aaronliu0130@gmail.com" },
+    { name = "John Vandenberg", email = "jayvdb@gmail.com" }
+]
+authors = [
+    { name = "Google Inc."},
+    { name = "Thibault Kruse"},
+    { name = "Andrew Davis", email = "theandrewdavis@gmail.com" }
+]
+description="Check C++ files configurably against Google's style guide"
+readme = "README.rst"
+requires-python = ">=3.8"
+keywords = ["lint", "cpp", "c++", "google style"]
+license = { text = "BSD-3-Clause" }
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: BSD License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
+    "Topic :: Software Development :: Quality Assurance",
+]
+
+[tool.setuptools.dynamic]
+version = {attr = "cpplint.__VERSION__"}
+
+[tool.setuptools.dynamic.optional-dependencies]
path: pyproject.toml
user: login: cclauss
body: Why store requirements in separate files?  Bringing them into pyproject.toml streamlines project config.

Why not unify `test` and `dev` into just `dev`?
created_at: 2025-03-04T08:35:00Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,9 @@
 [tox]
-envlist = py38, py39, py3.10, py311, py312, pypy3
+envlist = py3, pypy3
path: tox.ini
user: login: cclauss
body: This changes how tox runs locally.  If I use pyenv or uv to install multiple Python versions on my machine, then the current form runs the tests on each of the listed Python versions that exist.  Does the proposed form do the same?
created_at: 2025-03-04T08:35:45Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,9 @@
 [tox]
-envlist = py38, py39, py3.10, py311, py312, pypy3
+envlist = py3, pypy3
 skip_missing_interpreters = true
 
-[gh-actions]
-python =
-    3.8: py38
-    3.9: py39
-    3.10: py310
-    3.11: py311
-    3.12: py312
-    pypy3.10: pypy3
-
 [testenv]
-extras = dev
+extras = test
path: tox.ini
user: login: cclauss
body: dev or test? (See above.)
created_at: 2025-03-04T08:36:25Z
author_association: MEMBER
diff_hunk: @@ -14,7 +14,7 @@ jobs:
     strategy:
       matrix:
         # Python 3.8 is the last non-EOL version. Also adapt tox.ini
-        python-version: ['3.8', 'pypy3.10', '3.12']
+        python-version: ['3.8', 'pypy3.10', '3.x']
path: .github/workflows/ci.yml
user: login: aaronliu0130
body: 1. There's a fairly big bug (namespace indentation false positives) that will be fixed in the next release. I'd prefer we drop 3.8 after the next release.
2. I don't like taking commit authorship for others' ideas.
created_at: 2025-03-04T23:53:22Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,49 @@
 [build-system]
-build-backend = "setuptools.build_meta:__legacy__"
+build-backend = "setuptools.build_meta"
 requires = [
     "setuptools",
     "wheel",
 ]
+
+[project]
+name = "cpplint"
+dynamic = ["version", "optional-dependencies"]
+maintainers = [
+    { name = "Aaron Liu", email = "aaronliu0130@gmail.com" },
+    { name = "John Vandenberg", email = "jayvdb@gmail.com" }
+]
+authors = [
+    { name = "Google Inc."},
+    { name = "Thibault Kruse"},
+    { name = "Andrew Davis", email = "theandrewdavis@gmail.com" }
+]
+description="Check C++ files configurably against Google's style guide"
+readme = "README.rst"
+requires-python = ">=3.8"
+keywords = ["lint", "cpp", "c++", "google style"]
+license = { text = "BSD-3-Clause" }
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: BSD License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
+    "Topic :: Software Development :: Quality Assurance",
+]
+
+[tool.setuptools.dynamic]
+version = {attr = "cpplint.__VERSION__"}
+
+[tool.setuptools.dynamic.optional-dependencies]
path: pyproject.toml
user: login: aaronliu0130
body: It just felt more understandable to me for aesthetic reasons. No objections if you merge them into the TOML.

As for test/dev unification... good question. This does seem like excessive categorization. Please go ahead and make the commits to this PR for unifying the extras (and updating the documentation to reflect that).
created_at: 2025-03-05T00:04:39Z
author_association: MEMBER
diff_hunk: @@ -1,18 +1,9 @@
 [tox]
-envlist = py38, py39, py3.10, py311, py312, pypy3
+envlist = py3, pypy3
path: tox.ini
user: login: aaronliu0130
body: You're right, it seems to only test the latest versions after this change. Do we need the full envlist, though? Anyone can specify the environments through the command line.
We can definitely use the full envlist if needed though. We have to chore-update the PyPI Python version classifiers anyways.
created_at: 2025-03-05T03:03:54Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,49 @@
 [build-system]
-build-backend = "setuptools.build_meta:__legacy__"
+build-backend = "setuptools.build_meta"
 requires = [
     "setuptools",
     "wheel",
 ]
+
+[project]
+name = "cpplint"
+dynamic = ["version", "optional-dependencies"]
+maintainers = [
+    { name = "Aaron Liu", email = "aaronliu0130@gmail.com" },
+    { name = "John Vandenberg", email = "jayvdb@gmail.com" }
+]
+authors = [
+    { name = "Google Inc."},
+    { name = "Thibault Kruse"},
+    { name = "Andrew Davis", email = "theandrewdavis@gmail.com" }
+]
+description="Check C++ files configurably against Google's style guide"
+readme = "README.rst"
+requires-python = ">=3.8"
+keywords = ["lint", "cpp", "c++", "google style"]
+license = { text = "BSD-3-Clause" }
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: BSD License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
+    "Topic :: Software Development :: Quality Assurance",
+]
+
+[tool.setuptools.dynamic]
+version = {attr = "cpplint.__VERSION__"}
+
+[tool.setuptools.dynamic.optional-dependencies]
path: pyproject.toml
user: login: aaronliu0130
body: test and dev have now been unified.
created_at: 2025-03-05T03:19:00Z
author_association: MEMBER
diff_hunk: @@ -37,33 +47,67 @@ optional-dependencies.dev = [
   "pytest",
   "pytest-cov",
   "pytest-timeout",
-  "setuptools",
-  "testfixtures",
-  "tox<5",
-]
-optional-dependencies.test = [
-  "parameterized",
-  "pytest",
-  "pytest-cov",
-  "pytest-timeout",
   "testfixtures",
   "tox<5",
 ]
-optional-dependencies.testing = [
-  "parameterized",
-  "pytest",
-  "pytest-cov",
-  "pytest-timeout",
-  "testfixtures",
-  "tox<5",
-]
-urls.Download = "https://github.com/cpplint/cpplint"
 urls.Homepage = "https://github.com/cpplint/cpplint"
 scripts.cpplint = "cpplint:main"
 
 [tool.setuptools]
 py-modules = [ "cpplint" ]
 include-package-data = false
+dynamic.version = { attr = "cpplint.__VERSION__" }
+
+[tool.pylint.basic]
+include-naming-hint = true
+method-rgx = "[A-Z_][A-Za-z0-9]{2,49}$|__init__|__str__|__contains__"
+function-rgx = "[A-Z_][A-Za-z0-9]{2,49}$|main"
+const-rgx = "[a-zA-Z_][A-Za-z0-9_]{2,49}$"
+variable-rgx = "[a-z_][a-z0-9_]{0,49}$"
+argument-rgx = "[a-z_][a-z0-9_]{0,49}$"
+class-rgx = "[A-Z_][a-zA-Z0-9]+$|basestring|unicode|long|xrange"
path: pyproject.toml
user: login: cclauss
body: `basestring|unicode|long|xrange` were all Python 2 keywords that stopped being supported on 1/1/2020.  Why do we still need them?!?
created_at: 2025-03-08T21:55:46Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: cclauss
body: Do we still need https://github.com/wolever/parameterized that has had no update in two years, when pytest has https://docs.pytest.org/en/stable/how-to/parametrize.html#parametrize builtin?
created_at: 2025-03-08T22:01:31Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: aaronliu0130
body: That was the first thing I tried. I couldn't get that to work with the unittest-module–subclass setup in this repo. (Plas speling is very importent.)
created_at: 2025-03-08T22:19:23Z
author_association: MEMBER
diff_hunk: @@ -37,33 +47,67 @@ optional-dependencies.dev = [
   "pytest",
   "pytest-cov",
   "pytest-timeout",
-  "setuptools",
-  "testfixtures",
-  "tox<5",
-]
-optional-dependencies.test = [
-  "parameterized",
-  "pytest",
-  "pytest-cov",
-  "pytest-timeout",
   "testfixtures",
   "tox<5",
 ]
-optional-dependencies.testing = [
-  "parameterized",
-  "pytest",
-  "pytest-cov",
-  "pytest-timeout",
-  "testfixtures",
-  "tox<5",
-]
-urls.Download = "https://github.com/cpplint/cpplint"
 urls.Homepage = "https://github.com/cpplint/cpplint"
 scripts.cpplint = "cpplint:main"
 
 [tool.setuptools]
 py-modules = [ "cpplint" ]
 include-package-data = false
+dynamic.version = { attr = "cpplint.__VERSION__" }
+
+[tool.pylint.basic]
+include-naming-hint = true
+method-rgx = "[A-Z_][A-Za-z0-9]{2,49}$|__init__|__str__|__contains__"
+function-rgx = "[A-Z_][A-Za-z0-9]{2,49}$|main"
+const-rgx = "[a-zA-Z_][A-Za-z0-9_]{2,49}$"
+variable-rgx = "[a-z_][a-z0-9_]{0,49}$"
+argument-rgx = "[a-z_][a-z0-9_]{0,49}$"
+class-rgx = "[A-Z_][a-zA-Z0-9]+$|basestring|unicode|long|xrange"
path: pyproject.toml
user: login: aaronliu0130
body: Good question.
created_at: 2025-03-08T22:21:16Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: cclauss
body: It is not tested on Python 3.12, 3.13, and pytest > v3 (current is v8).  See the open issues and
* wolever/parameterized#182
created_at: 2025-03-09T06:12:37Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: aaronliu0130
body: Well, this seems like the best thing we have. We could always find an alternative solution later. It doesn't seem like parameterized's actual functionality we use has been impacted by their indeed-worrying neglect yet. (This was a reason I also tried the subtests approach, which works without additional modules but doesn't allow for -k filtering.)
created_at: 2025-03-09T17:05:31Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: aaronliu0130
body: See also https://stackoverflow.com/questions/18182251/does-pytest-parametrized-test-work-with-unittest-class-based-tests .
created_at: 2025-03-09T17:07:14Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: cclauss
body: With #332 I do not see why we need `unittest.TestCase` classes.  I will do more work on that today.
created_at: 2025-03-10T06:49:27Z
author_association: MEMBER
diff_hunk: @@ -38,50 +38,52 @@
 import unittest
 import shutil
 import tempfile
+
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
 
 BASE_CMD = sys.executable + ' ' + os.path.abspath('./cpplint.py ')
 
-def RunShellCommand(cmd: str, args: str, cwd='.'):
-    """
-    executes a command
-    :param cmd: A string to execute.
-    :param cwd: from which folder to run.
-    """
 
-    stdout_target = subprocess.PIPE
-    stderr_target = subprocess.PIPE
+def run_shell_command(cmd: str, args: str, cwd='.'):
+    """Executes a command
 
-    proc = subprocess.Popen(cmd + ' ' + args,
-                            shell=True,
-                            cwd=cwd,
-                            stdout=stdout_target,
-                            stderr=stderr_target)
-    out, err = proc.communicate()
+    Args:
+        cmd: A string to execute.
+        args: A string with arguments to the command.
+        cwd: from which folder to run.
+    """
+    cmd, args = cmd.split(), args.split()
+    proc = subprocess.run(cmd + args, cwd=cwd, capture_output=True)
+    out, err = proc.stdout, proc.stderr
 
     # Make output system-agnostic, aka support Windows
     if os.sep == '\\':
-        # TODO: Support scenario with multiple folder inputs
-        win_path = (os.path.dirname(args.split(' ')[-1]) + '\\').encode()
+        # TODO: Support scenario with multiple input names
+        # We currently only support the last arguments as the input name
+        # to prevent accidentally replacing sed tests.
+        # Fixing would likely need coding an internal "replace slashes" option for cpplint itself.
+        win_path = (os.path.dirname(args[-1]) + '\\').encode()
         good_path = win_path.replace(b'\\', b'/')
         out, err = out.replace(win_path, good_path), err.replace(win_path, good_path)
     if os.linesep == '\r\n':
         out, err = out.replace(b'\r\n', b'\n'), err.replace(b'\r\n', b'\n')
 
     # print(err) # to get the output at time of test
-    return (proc.returncode, out, err)
+    return proc.returncode, out, err
 
 
 class UsageTest(unittest.TestCase):
 
     def testHelp(self):
-        (status, out, err) = RunShellCommand(BASE_CMD, '--help')
+        (status, out, err) = run_shell_command(BASE_CMD, '--help')
         self.assertEqual(0, status)
         self.assertEqual(b'', out)
         self.assertTrue(err.startswith(b'\nSyntax: cpplint'))
 
-class TemporaryFolderClassSetup(object):
+
+class TemporaryFolderClassSetup(unittest.TestCase):
path: cpplint_clitest.py
user: login: cclauss
body: Why this change?

Can we remove it and use pytest parameterize instead of the unmaintained library?
created_at: 2025-03-10T06:54:13Z
author_association: MEMBER
diff_hunk: @@ -38,50 +38,52 @@
 import unittest
 import shutil
 import tempfile
+
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
 
 BASE_CMD = sys.executable + ' ' + os.path.abspath('./cpplint.py ')
 
-def RunShellCommand(cmd: str, args: str, cwd='.'):
-    """
-    executes a command
-    :param cmd: A string to execute.
-    :param cwd: from which folder to run.
-    """
 
-    stdout_target = subprocess.PIPE
-    stderr_target = subprocess.PIPE
+def run_shell_command(cmd: str, args: str, cwd='.'):
+    """Executes a command
 
-    proc = subprocess.Popen(cmd + ' ' + args,
-                            shell=True,
-                            cwd=cwd,
-                            stdout=stdout_target,
-                            stderr=stderr_target)
-    out, err = proc.communicate()
+    Args:
+        cmd: A string to execute.
+        args: A string with arguments to the command.
+        cwd: from which folder to run.
+    """
+    cmd, args = cmd.split(), args.split()
+    proc = subprocess.run(cmd + args, cwd=cwd, capture_output=True)
+    out, err = proc.stdout, proc.stderr
 
     # Make output system-agnostic, aka support Windows
     if os.sep == '\\':
-        # TODO: Support scenario with multiple folder inputs
-        win_path = (os.path.dirname(args.split(' ')[-1]) + '\\').encode()
+        # TODO: Support scenario with multiple input names
+        # We currently only support the last arguments as the input name
+        # to prevent accidentally replacing sed tests.
+        # Fixing would likely need coding an internal "replace slashes" option for cpplint itself.
+        win_path = (os.path.dirname(args[-1]) + '\\').encode()
         good_path = win_path.replace(b'\\', b'/')
         out, err = out.replace(win_path, good_path), err.replace(win_path, good_path)
     if os.linesep == '\r\n':
         out, err = out.replace(b'\r\n', b'\n'), err.replace(b'\r\n', b'\n')
 
     # print(err) # to get the output at time of test
-    return (proc.returncode, out, err)
+    return proc.returncode, out, err
 
 
 class UsageTest(unittest.TestCase):
 
     def testHelp(self):
-        (status, out, err) = RunShellCommand(BASE_CMD, '--help')
+        (status, out, err) = run_shell_command(BASE_CMD, '--help')
         self.assertEqual(0, status)
         self.assertEqual(b'', out)
         self.assertTrue(err.startswith(b'\nSyntax: cpplint'))
 
-class TemporaryFolderClassSetup(object):
+
+class TemporaryFolderClassSetup(unittest.TestCase):
path: cpplint_clitest.py
user: login: cclauss
body: https://github.com/pytest-dev/pytest-subtests
created_at: 2025-03-10T10:35:27Z
author_association: MEMBER
diff_hunk: @@ -38,50 +38,52 @@
 import unittest
 import shutil
 import tempfile
+
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
 
 BASE_CMD = sys.executable + ' ' + os.path.abspath('./cpplint.py ')
 
-def RunShellCommand(cmd: str, args: str, cwd='.'):
-    """
-    executes a command
-    :param cmd: A string to execute.
-    :param cwd: from which folder to run.
-    """
 
-    stdout_target = subprocess.PIPE
-    stderr_target = subprocess.PIPE
+def run_shell_command(cmd: str, args: str, cwd='.'):
+    """Executes a command
 
-    proc = subprocess.Popen(cmd + ' ' + args,
-                            shell=True,
-                            cwd=cwd,
-                            stdout=stdout_target,
-                            stderr=stderr_target)
-    out, err = proc.communicate()
+    Args:
+        cmd: A string to execute.
+        args: A string with arguments to the command.
+        cwd: from which folder to run.
+    """
+    cmd, args = cmd.split(), args.split()
+    proc = subprocess.run(cmd + args, cwd=cwd, capture_output=True)
+    out, err = proc.stdout, proc.stderr
 
     # Make output system-agnostic, aka support Windows
     if os.sep == '\\':
-        # TODO: Support scenario with multiple folder inputs
-        win_path = (os.path.dirname(args.split(' ')[-1]) + '\\').encode()
+        # TODO: Support scenario with multiple input names
+        # We currently only support the last arguments as the input name
+        # to prevent accidentally replacing sed tests.
+        # Fixing would likely need coding an internal "replace slashes" option for cpplint itself.
+        win_path = (os.path.dirname(args[-1]) + '\\').encode()
         good_path = win_path.replace(b'\\', b'/')
         out, err = out.replace(win_path, good_path), err.replace(win_path, good_path)
     if os.linesep == '\r\n':
         out, err = out.replace(b'\r\n', b'\n'), err.replace(b'\r\n', b'\n')
 
     # print(err) # to get the output at time of test
-    return (proc.returncode, out, err)
+    return proc.returncode, out, err
 
 
 class UsageTest(unittest.TestCase):
 
     def testHelp(self):
-        (status, out, err) = RunShellCommand(BASE_CMD, '--help')
+        (status, out, err) = run_shell_command(BASE_CMD, '--help')
         self.assertEqual(0, status)
         self.assertEqual(b'', out)
         self.assertTrue(err.startswith(b'\nSyntax: cpplint'))
 
-class TemporaryFolderClassSetup(object):
+
+class TemporaryFolderClassSetup(unittest.TestCase):
path: cpplint_clitest.py
user: login: aaronliu0130
body: This is just a linting thing to show the override hints in PyCharm. It works exactly the same without this change.

I've mentioned that subtests were tried and not filterable in a comment above.
created_at: 2025-03-10T10:54:28Z
author_association: MEMBER
diff_hunk: @@ -38,50 +38,52 @@
 import unittest
 import shutil
 import tempfile
+
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
 
 BASE_CMD = sys.executable + ' ' + os.path.abspath('./cpplint.py ')
 
-def RunShellCommand(cmd: str, args: str, cwd='.'):
-    """
-    executes a command
-    :param cmd: A string to execute.
-    :param cwd: from which folder to run.
-    """
 
-    stdout_target = subprocess.PIPE
-    stderr_target = subprocess.PIPE
+def run_shell_command(cmd: str, args: str, cwd='.'):
+    """Executes a command
 
-    proc = subprocess.Popen(cmd + ' ' + args,
-                            shell=True,
-                            cwd=cwd,
-                            stdout=stdout_target,
-                            stderr=stderr_target)
-    out, err = proc.communicate()
+    Args:
+        cmd: A string to execute.
+        args: A string with arguments to the command.
+        cwd: from which folder to run.
+    """
+    cmd, args = cmd.split(), args.split()
+    proc = subprocess.run(cmd + args, cwd=cwd, capture_output=True)
+    out, err = proc.stdout, proc.stderr
 
     # Make output system-agnostic, aka support Windows
     if os.sep == '\\':
-        # TODO: Support scenario with multiple folder inputs
-        win_path = (os.path.dirname(args.split(' ')[-1]) + '\\').encode()
+        # TODO: Support scenario with multiple input names
+        # We currently only support the last arguments as the input name
+        # to prevent accidentally replacing sed tests.
+        # Fixing would likely need coding an internal "replace slashes" option for cpplint itself.
+        win_path = (os.path.dirname(args[-1]) + '\\').encode()
         good_path = win_path.replace(b'\\', b'/')
         out, err = out.replace(win_path, good_path), err.replace(win_path, good_path)
     if os.linesep == '\r\n':
         out, err = out.replace(b'\r\n', b'\n'), err.replace(b'\r\n', b'\n')
 
     # print(err) # to get the output at time of test
-    return (proc.returncode, out, err)
+    return proc.returncode, out, err
 
 
 class UsageTest(unittest.TestCase):
 
     def testHelp(self):
-        (status, out, err) = RunShellCommand(BASE_CMD, '--help')
+        (status, out, err) = run_shell_command(BASE_CMD, '--help')
         self.assertEqual(0, status)
         self.assertEqual(b'', out)
         self.assertTrue(err.startswith(b'\nSyntax: cpplint'))
 
-class TemporaryFolderClassSetup(object):
+
+class TemporaryFolderClassSetup(unittest.TestCase):
path: cpplint_clitest.py
user: login: cclauss
body: You tried with `pytest-subtests` instead of `wolever/parameterized`?
created_at: 2025-03-10T11:39:04Z
author_association: MEMBER
diff_hunk: @@ -38,50 +38,52 @@
 import unittest
 import shutil
 import tempfile
+
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
 
 BASE_CMD = sys.executable + ' ' + os.path.abspath('./cpplint.py ')
 
-def RunShellCommand(cmd: str, args: str, cwd='.'):
-    """
-    executes a command
-    :param cmd: A string to execute.
-    :param cwd: from which folder to run.
-    """
 
-    stdout_target = subprocess.PIPE
-    stderr_target = subprocess.PIPE
+def run_shell_command(cmd: str, args: str, cwd='.'):
+    """Executes a command
 
-    proc = subprocess.Popen(cmd + ' ' + args,
-                            shell=True,
-                            cwd=cwd,
-                            stdout=stdout_target,
-                            stderr=stderr_target)
-    out, err = proc.communicate()
+    Args:
+        cmd: A string to execute.
+        args: A string with arguments to the command.
+        cwd: from which folder to run.
+    """
+    cmd, args = cmd.split(), args.split()
+    proc = subprocess.run(cmd + args, cwd=cwd, capture_output=True)
+    out, err = proc.stdout, proc.stderr
 
     # Make output system-agnostic, aka support Windows
     if os.sep == '\\':
-        # TODO: Support scenario with multiple folder inputs
-        win_path = (os.path.dirname(args.split(' ')[-1]) + '\\').encode()
+        # TODO: Support scenario with multiple input names
+        # We currently only support the last arguments as the input name
+        # to prevent accidentally replacing sed tests.
+        # Fixing would likely need coding an internal "replace slashes" option for cpplint itself.
+        win_path = (os.path.dirname(args[-1]) + '\\').encode()
         good_path = win_path.replace(b'\\', b'/')
         out, err = out.replace(win_path, good_path), err.replace(win_path, good_path)
     if os.linesep == '\r\n':
         out, err = out.replace(b'\r\n', b'\n'), err.replace(b'\r\n', b'\n')
 
     # print(err) # to get the output at time of test
-    return (proc.returncode, out, err)
+    return proc.returncode, out, err
 
 
 class UsageTest(unittest.TestCase):
 
     def testHelp(self):
-        (status, out, err) = RunShellCommand(BASE_CMD, '--help')
+        (status, out, err) = run_shell_command(BASE_CMD, '--help')
         self.assertEqual(0, status)
         self.assertEqual(b'', out)
         self.assertTrue(err.startswith(b'\nSyntax: cpplint'))
 
-class TemporaryFolderClassSetup(object):
+
+class TemporaryFolderClassSetup(unittest.TestCase):
path: cpplint_clitest.py
user: login: aaronliu0130
body: I don't think parameterized even has subtests support. Yes, I tried both unitest's own subtests and pytest-subtests. See https://github.com/pytest-dev/pytest-subtests/issues/100.
created_at: 2025-03-10T12:53:11Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: aaronliu0130
body: I think we'd have a less messy branch history if we just merge this now while you work on that.
created_at: 2025-03-10T12:53:52Z
author_association: MEMBER
diff_hunk: @@ -176,29 +178,22 @@ class NoRepoSignatureTests(TemporaryFolderClassSetup, unittest.TestCase):
     """runs in a temporary folder (under /tmp in linux) without any .git/.hg/.svn file"""
 
     def get_extra_command_args(self, cwd):
-        return (' --repository %s ' % self._root)
-
-    def testChromiumSample(self):
-        self.checkAllInFolder('./samples/chromium-sample', 1)
-
-    def testVlcSample(self):
-        self.checkAllInFolder('./samples/vlc-sample', 1)
-
-    def testSillySample(self):
-        self.checkAllInFolder('./samples/silly-sample', 5)
-
-    def testBoostSample(self):
-        self.checkAllInFolder('./samples/boost-sample', 4)
-
+        return f' --repository {self._root} '
+
+    def _test_name_func(fun, _, x):
+        del fun
+        return f'test{x.args[0].capitalize()}Sample-{x.args[1]}'
+
+    @parameterized.expand([(folder, case[:-4])
path: cpplint_clitest.py
user: login: cclauss
body: #322 has now removed all instances of `unittest.TestCase`.
created_at: 2025-03-10T15:57:37Z
author_association: MEMBER
diff_hunk: @@ -38,50 +38,52 @@
 import unittest
 import shutil
 import tempfile
+
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
 
 BASE_CMD = sys.executable + ' ' + os.path.abspath('./cpplint.py ')
 
-def RunShellCommand(cmd: str, args: str, cwd='.'):
-    """
-    executes a command
-    :param cmd: A string to execute.
-    :param cwd: from which folder to run.
-    """
 
-    stdout_target = subprocess.PIPE
-    stderr_target = subprocess.PIPE
+def run_shell_command(cmd: str, args: str, cwd='.'):
+    """Executes a command
 
-    proc = subprocess.Popen(cmd + ' ' + args,
-                            shell=True,
-                            cwd=cwd,
-                            stdout=stdout_target,
-                            stderr=stderr_target)
-    out, err = proc.communicate()
+    Args:
+        cmd: A string to execute.
+        args: A string with arguments to the command.
+        cwd: from which folder to run.
+    """
+    cmd, args = cmd.split(), args.split()
+    proc = subprocess.run(cmd + args, cwd=cwd, capture_output=True)
+    out, err = proc.stdout, proc.stderr
 
     # Make output system-agnostic, aka support Windows
     if os.sep == '\\':
-        # TODO: Support scenario with multiple folder inputs
-        win_path = (os.path.dirname(args.split(' ')[-1]) + '\\').encode()
+        # TODO: Support scenario with multiple input names
+        # We currently only support the last arguments as the input name
+        # to prevent accidentally replacing sed tests.
+        # Fixing would likely need coding an internal "replace slashes" option for cpplint itself.
+        win_path = (os.path.dirname(args[-1]) + '\\').encode()
         good_path = win_path.replace(b'\\', b'/')
         out, err = out.replace(win_path, good_path), err.replace(win_path, good_path)
     if os.linesep == '\r\n':
         out, err = out.replace(b'\r\n', b'\n'), err.replace(b'\r\n', b'\n')
 
     # print(err) # to get the output at time of test
-    return (proc.returncode, out, err)
+    return proc.returncode, out, err
 
 
 class UsageTest(unittest.TestCase):
 
     def testHelp(self):
-        (status, out, err) = RunShellCommand(BASE_CMD, '--help')
+        (status, out, err) = run_shell_command(BASE_CMD, '--help')
         self.assertEqual(0, status)
         self.assertEqual(b'', out)
         self.assertTrue(err.startswith(b'\nSyntax: cpplint'))
 
-class TemporaryFolderClassSetup(object):
+
+class TemporaryFolderClassSetup(unittest.TestCase):
path: cpplint_clitest.py
user: login: cclauss
body: #332 has now removed all instances of `unittest.TestCase`.
created_at: 2025-03-10T15:58:54Z
author_association: MEMBER
diff_hunk: @@ -41,48 +41,49 @@
 
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
path: cpplint_clitest.py
user: login: cclauss
body: Move this line just above pytest
created_at: 2025-03-11T22:10:57Z
author_association: MEMBER
diff_hunk: @@ -41,48 +41,49 @@
 
 from pytest import mark
 from testfixtures import compare
+from parameterized import parameterized
path: cpplint_clitest.py
user: login: aaronliu0130
body: Yeah. I'm also working on a rebase so that this PR can be merged by rebase rn.
created_at: 2025-03-11T22:12:23Z
author_association: MEMBER

PR comments:
user: login: cclauss
created_at: 2025-03-04T08:37:34Z
body: In the commit message, is it a new version of `pytest` or of `pylint`?
author_association: MEMBER
user: login: aaronliu0130
created_at: 2025-03-04T23:48:59Z
body: The commit messages correctly said pytest; my note at the top about a new version of pylint didn't.
author_association: MEMBER
user: login: aaronliu0130
created_at: 2025-03-08T22:27:14Z
body: Interestingly, the pre-commit CI is complaining about something that's not even part of this PR's base yet. Will rebase.
author_association: MEMBER
user: login: aaronliu0130
created_at: 2025-03-09T21:06:25Z
body: @jayvdb @cclauss Hopefully we can merge this and release 2.0.1 soon!
author_association: MEMBER
user: login: aaronliu0130
created_at: 2025-03-10T17:18:38Z
body: That is still a draft, no? Do you want to me to replace your PR's unittest file with the black-formatted one I provided and then merge it?

author_association: MEMBER
user: login: cclauss
created_at: 2025-03-11T22:02:26Z
body: Please rebase this so we can merge it.
author_association: MEMBER