From 4c3c39a1ea9a3bfe017e49fce8d94727a96e6932 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sat, 21 Jun 2025 19:21:13 +0000 Subject: [PATCH 01/17] iadd support for the envvar and implement tests --- Lib/netrc.py | 2 +- Lib/test/test_netrc.py | 109 +++++++++++++++++++++++++++++------------ 2 files changed, 80 insertions(+), 31 deletions(-) diff --git a/Lib/netrc.py b/Lib/netrc.py index b285fd8e357ddb..f2d56d0fac1de3 100644 --- a/Lib/netrc.py +++ b/Lib/netrc.py @@ -67,7 +67,7 @@ class netrc: def __init__(self, file=None): default_netrc = file is None if file is None: - file = os.path.join(os.path.expanduser("~"), ".netrc") + file = os.environ.get("NETRC", "") or os.path.join(os.path.expanduser("~"), ".netrc") self.hosts = {} self.macros = {} try: diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 81e11a293cc4c8..d2b88b19df832d 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -1,45 +1,92 @@ -import netrc, os, unittest, sys, textwrap -from test.support import os_helper +import netrc, os, unittest, sys, textwrap, tempfile + +from test import support +from unittest import mock try: import pwd except ImportError: pwd = None -temp_filename = os_helper.TESTFN + +def generate_netrc(directory, filename, data): + data = textwrap.dedent(data) + mode = 'w' + mode = 'w' + if sys.platform != 'cygwin': + mode += 't' + with open(os.path.join(directory, filename), mode, encoding="utf-8") as fp: + fp.write(data) + class NetrcTestCase(unittest.TestCase): - def make_nrc(self, test_data): - test_data = textwrap.dedent(test_data) - mode = 'w' - if sys.platform != 'cygwin': - mode += 't' - with open(temp_filename, mode, encoding="utf-8") as fp: - fp.write(test_data) - try: - nrc = netrc.netrc(temp_filename) - finally: - os.unlink(temp_filename) + @staticmethod + def home_netrc(data): + with support.os_helper.EnvironmentVarGuard() as environ, \ + tempfile.TemporaryDirectory() as tmpdir: + environ.unset('NETRC') + environ.unset('HOME') + + generate_netrc(tmpdir, ".netrc", data) + os.chmod(os.path.join(tmpdir, ".netrc"), 0o600) + + with mock.patch("os.path.expanduser"): + os.path.expanduser.return_value = tmpdir + nrc = netrc.netrc() + return nrc - def test_toplevel_non_ordered_tokens(self): - nrc = self.make_nrc("""\ + @staticmethod + def envvar_netrc(data): + with support.os_helper.EnvironmentVarGuard() as environ: + with tempfile.TemporaryDirectory() as tmpdir: + environ.set('NETRC', os.path.join(tmpdir, ".netrc")) + environ.unset('HOME') + + generate_netrc(tmpdir, ".netrc", data) + os.chmod(os.path.join(tmpdir, ".netrc"), 0o600) + + nrc = netrc.netrc() + + return nrc + + @staticmethod + def file_argument(data): + with support.os_helper.EnvironmentVarGuard() as environ: + with tempfile.TemporaryDirectory() as tmpdir: + environ.set('NETRC', 'not-a-file.random') + + generate_netrc(tmpdir, ".netrc", data) + os.chmod(os.path.join(tmpdir, ".netrc"), 0o600) + + nrc = netrc.netrc(os.path.join(tmpdir, ".netrc")) + + return nrc + + def make_nrc(self, test_data): + return self.file_argument(test_data) + + @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) + def test_toplevel_non_ordered_tokens(self, nrc_builder): + nrc = nrc_builder("""\ machine host.domain.com password pass1 login log1 account acct1 default login log2 password pass2 account acct2 """) self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - def test_toplevel_tokens(self): - nrc = self.make_nrc("""\ + @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) + def test_toplevel_tokens(self, nrc_builder): + nrc = nrc_builder("""\ machine host.domain.com login log1 password pass1 account acct1 default login log2 password pass2 account acct2 """) self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - def test_macros(self): + @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) + def test_macros(self, nrc_builder): data = """\ macdef macro1 line1 @@ -50,14 +97,15 @@ def test_macros(self): line4 """ - nrc = self.make_nrc(data) + nrc = nrc_builder(data) self.assertEqual(nrc.macros, {'macro1': ['line1\n', 'line2\n'], 'macro2': ['line3\n', 'line4\n']}) # strip the last \n self.assertRaises(netrc.NetrcParseError, self.make_nrc, data.rstrip(' ')[:-1]) - def test_optional_tokens(self): + @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) + def test_optional_tokens(self, nrc_builder): data = ( "machine host.domain.com", "machine host.domain.com login", @@ -68,7 +116,7 @@ def test_optional_tokens(self): "machine host.domain.com account \"\" password" ) for item in data: - nrc = self.make_nrc(item) + nrc = nrc_builder(item) self.assertEqual(nrc.hosts['host.domain.com'], ('', '', '')) data = ( "default", @@ -83,7 +131,8 @@ def test_optional_tokens(self): nrc = self.make_nrc(item) self.assertEqual(nrc.hosts['default'], ('', '', '')) - def test_invalid_tokens(self): + @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) + def test_invalid_tokens(self, nrc_builder): data = ( "invalid host.domain.com", "machine host.domain.com invalid", @@ -92,7 +141,7 @@ def test_invalid_tokens(self): "default host.domain.com login log password pass account acct invalid" ) for item in data: - self.assertRaises(netrc.NetrcParseError, self.make_nrc, item) + self.assertRaises(netrc.NetrcParseError, nrc_builder, item) def _test_token_x(self, nrc, token, value): nrc = self.make_nrc(nrc) @@ -102,7 +151,7 @@ def _test_token_x(self, nrc, token, value): self.assertEqual(nrc.hosts['host.domain.com'], ('log', value, 'pass')) elif token == 'password': self.assertEqual(nrc.hosts['host.domain.com'], ('log', 'acct', value)) - + def test_token_value_quotes(self): self._test_token_x("""\ machine host.domain.com login "log" password pass account acct @@ -272,20 +321,20 @@ def test_comment_at_end_of_machine_line_pass_has_hash(self): @unittest.skipUnless(os.name == 'posix', 'POSIX only test') @unittest.skipIf(pwd is None, 'security check requires pwd module') - @os_helper.skip_unless_working_chmod + @support.os_helper.skip_unless_working_chmod def test_security(self): # This test is incomplete since we are normally not run as root and # therefore can't test the file ownership being wrong. - d = os_helper.TESTFN + d = support.os_helper.TESTFN os.mkdir(d) - self.addCleanup(os_helper.rmtree, d) + self.addCleanup(support.os_helper.rmtree, d) fn = os.path.join(d, '.netrc') with open(fn, 'wt') as f: f.write("""\ machine foo.domain.com login bar password pass default login foo password pass """) - with os_helper.EnvironmentVarGuard() as environ: + with support.os_helper.EnvironmentVarGuard() as environ: environ.set('HOME', d) os.chmod(fn, 0o600) nrc = netrc.netrc() @@ -298,7 +347,7 @@ def test_security(self): machine foo.domain.com login anonymous password pass default login foo password pass """) - with os_helper.EnvironmentVarGuard() as environ: + with support.os_helper.EnvironmentVarGuard() as environ: environ.set('HOME', d) os.chmod(fn, 0o600) nrc = netrc.netrc() From 50ca1d0dca52a5edbb236a828f3ab8651b8c786f Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sat, 21 Jun 2025 22:20:22 +0000 Subject: [PATCH 02/17] refactor tests and double check logic for security --- Lib/netrc.py | 5 +- Lib/test/test_netrc.py | 335 +++++++++++++++++++++++------------------ 2 files changed, 192 insertions(+), 148 deletions(-) diff --git a/Lib/netrc.py b/Lib/netrc.py index f2d56d0fac1de3..e78b400d5278ea 100644 --- a/Lib/netrc.py +++ b/Lib/netrc.py @@ -65,9 +65,10 @@ def push_token(self, token): class netrc: def __init__(self, file=None): - default_netrc = file is None + netrc_envvar = os.environ.get("NETRC", "") + default_netrc = file is None and not bool(netrc_envvar) if file is None: - file = os.environ.get("NETRC", "") or os.path.join(os.path.expanduser("~"), ".netrc") + file = netrc_envvar or os.path.join(os.path.expanduser("~"), ".netrc") self.hosts = {} self.macros = {} try: diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index d2b88b19df832d..080c3b126b6187 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -2,6 +2,7 @@ from test import support from unittest import mock +from contextlib import ExitStack try: import pwd @@ -9,84 +10,89 @@ pwd = None -def generate_netrc(directory, filename, data): - data = textwrap.dedent(data) - mode = 'w' - mode = 'w' - if sys.platform != 'cygwin': - mode += 't' - with open(os.path.join(directory, filename), mode, encoding="utf-8") as fp: - fp.write(data) +class NetrcEnvironment: + def __enter__(self): + self.stack = ExitStack() + self.environ = self.stack.enter_context(support.os_helper.EnvironmentVarGuard()) + self.tmpdir = self.stack.enter_context(tempfile.TemporaryDirectory()) + return self + def __exit__(self, *ignore_exc): + self.stack.close() -class NetrcTestCase(unittest.TestCase): - - @staticmethod - def home_netrc(data): - with support.os_helper.EnvironmentVarGuard() as environ, \ - tempfile.TemporaryDirectory() as tmpdir: - environ.unset('NETRC') - environ.unset('HOME') + def generate_netrc(self, content, filename=".netrc", mode=0o600, encoding="utf-8"): + write_mode = "w" + if sys.platform != "cygwin": + write_mode += "t" + + netrc_file = os.path.join(self.tmpdir, filename) + with open(netrc_file, mode=write_mode, encoding=encoding) as fp: + fp.write(textwrap.dedent(content)) - generate_netrc(tmpdir, ".netrc", data) - os.chmod(os.path.join(tmpdir, ".netrc"), 0o600) + os.chmod(netrc_file, mode=mode) - with mock.patch("os.path.expanduser"): - os.path.expanduser.return_value = tmpdir - nrc = netrc.netrc() + return netrc_file - return nrc +class NetrcBuilder: @staticmethod - def envvar_netrc(data): - with support.os_helper.EnvironmentVarGuard() as environ: - with tempfile.TemporaryDirectory() as tmpdir: - environ.set('NETRC', os.path.join(tmpdir, ".netrc")) - environ.unset('HOME') + def use_default_netrc_in_home(*args, **kwargs): + with NetrcEnvironment() as helper: + helper.environ.unset("NETRC") + helper.environ.unset("HOME") + + helper.generate_netrc(*args, **kwargs) - generate_netrc(tmpdir, ".netrc", data) - os.chmod(os.path.join(tmpdir, ".netrc"), 0o600) + with mock.patch("os.path.expanduser"): + os.path.expanduser.return_value = helper.tmpdir + return netrc.netrc() - nrc = netrc.netrc() + @staticmethod + def use_netrc_envvar(*args, **kwargs): + with NetrcEnvironment() as helper: + netrc_file = helper.generate_netrc(*args, **kwargs) - return nrc + helper.environ.set("NETRC", netrc_file) + return netrc.netrc() @staticmethod - def file_argument(data): - with support.os_helper.EnvironmentVarGuard() as environ: - with tempfile.TemporaryDirectory() as tmpdir: - environ.set('NETRC', 'not-a-file.random') + def use_file_argument(*args, **kwargs): + with NetrcEnvironment() as helper: + helper.environ.set("NETRC", "not-a-file.netrc") - generate_netrc(tmpdir, ".netrc", data) - os.chmod(os.path.join(tmpdir, ".netrc"), 0o600) + netrc_file = helper.generate_netrc(*args, **kwargs) + return netrc.netrc(netrc_file) - nrc = netrc.netrc(os.path.join(tmpdir, ".netrc")) + @staticmethod + def use_all_strategies(): + return (NetrcBuilder.use_default_netrc_in_home, + NetrcBuilder.use_netrc_envvar, + NetrcBuilder.use_file_argument) - return nrc - def make_nrc(self, test_data): - return self.file_argument(test_data) - - @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) - def test_toplevel_non_ordered_tokens(self, nrc_builder): - nrc = nrc_builder("""\ +class NetrcTestCase(unittest.TestCase): + ALL_STRATEGIES = NetrcBuilder.use_all_strategies() + + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_toplevel_non_ordered_tokens(self, make_nrc): + nrc = make_nrc("""\ machine host.domain.com password pass1 login log1 account acct1 default login log2 password pass2 account acct2 """) self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) - def test_toplevel_tokens(self, nrc_builder): - nrc = nrc_builder("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_toplevel_tokens(self, make_nrc): + nrc = make_nrc("""\ machine host.domain.com login log1 password pass1 account acct1 default login log2 password pass2 account acct2 """) self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) - def test_macros(self, nrc_builder): + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_macros(self, make_nrc): data = """\ macdef macro1 line1 @@ -97,15 +103,15 @@ def test_macros(self, nrc_builder): line4 """ - nrc = nrc_builder(data) + nrc = make_nrc(data) self.assertEqual(nrc.macros, {'macro1': ['line1\n', 'line2\n'], 'macro2': ['line3\n', 'line4\n']}) # strip the last \n - self.assertRaises(netrc.NetrcParseError, self.make_nrc, + self.assertRaises(netrc.NetrcParseError, make_nrc, data.rstrip(' ')[:-1]) - @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) - def test_optional_tokens(self, nrc_builder): + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_optional_tokens(self, make_nrc): data = ( "machine host.domain.com", "machine host.domain.com login", @@ -116,7 +122,7 @@ def test_optional_tokens(self, nrc_builder): "machine host.domain.com account \"\" password" ) for item in data: - nrc = nrc_builder(item) + nrc = make_nrc(item) self.assertEqual(nrc.hosts['host.domain.com'], ('', '', '')) data = ( "default", @@ -128,11 +134,11 @@ def test_optional_tokens(self, nrc_builder): "default account \"\" password" ) for item in data: - nrc = self.make_nrc(item) + nrc = make_nrc(item) self.assertEqual(nrc.hosts['default'], ('', '', '')) - @support.subTests('nrc_builder', (home_netrc, envvar_netrc, file_argument)) - def test_invalid_tokens(self, nrc_builder): + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_invalid_tokens(self, make_nrc): data = ( "invalid host.domain.com", "machine host.domain.com invalid", @@ -141,10 +147,10 @@ def test_invalid_tokens(self, nrc_builder): "default host.domain.com login log password pass account acct invalid" ) for item in data: - self.assertRaises(netrc.NetrcParseError, nrc_builder, item) + self.assertRaises(netrc.NetrcParseError, make_nrc, item) - def _test_token_x(self, nrc, token, value): - nrc = self.make_nrc(nrc) + def _test_token_x(self, make_nrc, content, token, value): + nrc = make_nrc(content) if token == 'login': self.assertEqual(nrc.hosts['host.domain.com'], (value, 'acct', 'pass')) elif token == 'account': @@ -152,210 +158,247 @@ def _test_token_x(self, nrc, token, value): elif token == 'password': self.assertEqual(nrc.hosts['host.domain.com'], ('log', 'acct', value)) - def test_token_value_quotes(self): - self._test_token_x("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_token_value_quotes(self, make_nrc): + self._test_token_x(make_nrc, """\ machine host.domain.com login "log" password pass account acct """, 'login', 'log') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account "acct" """, 'account', 'acct') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password "pass" account acct """, 'password', 'pass') - def test_token_value_escape(self): - self._test_token_x("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_token_value_escape(self, make_nrc): + self._test_token_x(make_nrc, """\ machine host.domain.com login \\"log password pass account acct """, 'login', '"log') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login "\\"log" password pass account acct """, 'login', '"log') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account \\"acct """, 'account', '"acct') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account "\\"acct" """, 'account', '"acct') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password \\"pass account acct """, 'password', '"pass') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password "\\"pass" account acct """, 'password', '"pass') - def test_token_value_whitespace(self): - self._test_token_x("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_token_value_whitespace(self, make_nrc): + self._test_token_x(make_nrc, """\ machine host.domain.com login "lo g" password pass account acct """, 'login', 'lo g') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password "pas s" account acct """, 'password', 'pas s') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account "acc t" """, 'account', 'acc t') - def test_token_value_non_ascii(self): - self._test_token_x("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_token_value_non_ascii(self, make_nrc): + self._test_token_x(make_nrc, """\ machine host.domain.com login \xa1\xa2 password pass account acct """, 'login', '\xa1\xa2') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account \xa1\xa2 """, 'account', '\xa1\xa2') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password \xa1\xa2 account acct """, 'password', '\xa1\xa2') - def test_token_value_leading_hash(self): - self._test_token_x("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_token_value_leading_hash(self, make_nrc): + self._test_token_x(make_nrc, """\ machine host.domain.com login #log password pass account acct """, 'login', '#log') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account #acct """, 'account', '#acct') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password #pass account acct """, 'password', '#pass') - def test_token_value_trailing_hash(self): - self._test_token_x("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_token_value_trailing_hash(self, make_nrc): + self._test_token_x(make_nrc, """\ machine host.domain.com login log# password pass account acct """, 'login', 'log#') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account acct# """, 'account', 'acct#') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass# account acct """, 'password', 'pass#') - def test_token_value_internal_hash(self): - self._test_token_x("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_token_value_internal_hash(self, make_nrc): + self._test_token_x(make_nrc, """\ machine host.domain.com login lo#g password pass account acct """, 'login', 'lo#g') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pass account ac#ct """, 'account', 'ac#ct') - self._test_token_x("""\ + self._test_token_x(make_nrc, """\ machine host.domain.com login log password pa#ss account acct """, 'password', 'pa#ss') - def _test_comment(self, nrc, passwd='pass'): - nrc = self.make_nrc(nrc) + def _test_comment(self, make_nrc, content, passwd='pass'): + nrc = make_nrc(content) self.assertEqual(nrc.hosts['foo.domain.com'], ('bar', '', passwd)) self.assertEqual(nrc.hosts['bar.domain.com'], ('foo', '', 'pass')) - def test_comment_before_machine_line(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_before_machine_line(self, make_nrc): + self._test_comment(make_nrc, """\ # comment machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass """) - def test_comment_before_machine_line_no_space(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_before_machine_line_no_space(self, make_nrc): + self._test_comment(make_nrc, """\ #comment machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass """) - def test_comment_before_machine_line_hash_only(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_before_machine_line_hash_only(self, make_nrc): + self._test_comment(make_nrc, """\ # machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass """) - def test_comment_after_machine_line(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_after_machine_line(self, make_nrc): + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass # comment machine bar.domain.com login foo password pass """) - self._test_comment("""\ + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass # comment """) - def test_comment_after_machine_line_no_space(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_after_machine_line_no_space(self, make_nrc): + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass #comment machine bar.domain.com login foo password pass """) - self._test_comment("""\ + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass #comment """) - def test_comment_after_machine_line_hash_only(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_after_machine_line_hash_only(self, make_nrc): + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass # machine bar.domain.com login foo password pass """) - self._test_comment("""\ + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass # """) - def test_comment_at_end_of_machine_line(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_at_end_of_machine_line(self, make_nrc): + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass # comment machine bar.domain.com login foo password pass """) - def test_comment_at_end_of_machine_line_no_space(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_at_end_of_machine_line_no_space(self, make_nrc): + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass #comment machine bar.domain.com login foo password pass """) - def test_comment_at_end_of_machine_line_pass_has_hash(self): - self._test_comment("""\ + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_comment_at_end_of_machine_line_pass_has_hash(self, make_nrc): + self._test_comment(make_nrc, """\ machine foo.domain.com login bar password #pass #comment machine bar.domain.com login foo password pass """, '#pass') + @unittest.skipUnless(os.name == 'posix', 'POSIX only test') + @unittest.skipIf(pwd is None, 'security check requires pwd module') + @support.os_helper.skip_unless_working_chmod + def test_non_anonymous_security(self): + # This test is incomplete since we are normally not run as root and + # therefore can't test the file ownership being wrong. + content = """ + machine foo.domain.com login bar password pass + default login foo password pass + """ + mode = 0o662 + + # Use ~/.netrc and login is not anon + with self.assertRaises(netrc.NetrcParseError): + NetrcBuilder.use_default_netrc_in_home(content, mode=mode) + + # Don't use default file + nrc = NetrcBuilder.use_file_argument(content, mode=mode) + self.assertEqual(nrc.hosts['foo.domain.com'], + ('bar', '', 'pass')) @unittest.skipUnless(os.name == 'posix', 'POSIX only test') @unittest.skipIf(pwd is None, 'security check requires pwd module') @support.os_helper.skip_unless_working_chmod - def test_security(self): + @support.subTests('make_nrc', ALL_STRATEGIES) + def test_anonymous_security(self, make_nrc): # This test is incomplete since we are normally not run as root and # therefore can't test the file ownership being wrong. - d = support.os_helper.TESTFN - os.mkdir(d) - self.addCleanup(support.os_helper.rmtree, d) - fn = os.path.join(d, '.netrc') - with open(fn, 'wt') as f: - f.write("""\ - machine foo.domain.com login bar password pass - default login foo password pass - """) - with support.os_helper.EnvironmentVarGuard() as environ: - environ.set('HOME', d) - os.chmod(fn, 0o600) - nrc = netrc.netrc() - self.assertEqual(nrc.hosts['foo.domain.com'], - ('bar', '', 'pass')) - os.chmod(fn, 0o622) - self.assertRaises(netrc.NetrcParseError, netrc.netrc) - with open(fn, 'wt') as f: - f.write("""\ - machine foo.domain.com login anonymous password pass - default login foo password pass - """) - with support.os_helper.EnvironmentVarGuard() as environ: - environ.set('HOME', d) - os.chmod(fn, 0o600) - nrc = netrc.netrc() - self.assertEqual(nrc.hosts['foo.domain.com'], - ('anonymous', '', 'pass')) - os.chmod(fn, 0o622) + content = """\ + machine foo.domain.com login anonymous password pass + """ + mode = 0o662 + + # When it's anonymous, file permissions are not bypassed + nrc = make_nrc(content, mode=mode) + self.assertEqual(nrc.hosts['foo.domain.com'], + ('anonymous', '', 'pass')) + + @unittest.skipUnless(os.name == 'posix', 'POSIX only test') + @unittest.skipIf(pwd is None, 'security check requires pwd module') + @support.os_helper.skip_unless_working_chmod + def test_anonymous_security_with_default(self): + # This test is incomplete since we are normally not run as root and + # therefore can't test the file ownership being wrong. + content = """\ + machine foo.domain.com login anonymous password pass + default login foo password pass + """ + mode = 0o622 + + # "foo" is not anonymous, therefore the security check is triggered when we fallback to default netrc + with self.assertRaises(netrc.NetrcParseError): + NetrcBuilder.use_default_netrc_in_home(content, mode=mode) + + # Security check isn't triggered if the file is passed as environment variable or argument + for make_nrc in (NetrcBuilder.use_file_argument, NetrcBuilder.use_netrc_envvar): + nrc = make_nrc(content, mode=mode) self.assertEqual(nrc.hosts['foo.domain.com'], - ('anonymous', '', 'pass')) + ('anonymous', '', 'pass')) if __name__ == "__main__": From f17476bc28a3af935d611b6d19999d4702e2defc Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sat, 21 Jun 2025 22:38:21 +0000 Subject: [PATCH 03/17] add documentation --- Doc/library/netrc.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst index f6260383b2b057..8ae13e815dcf89 100644 --- a/Doc/library/netrc.rst +++ b/Doc/library/netrc.rst @@ -18,9 +18,10 @@ the Unix :program:`ftp` program and other FTP clients. .. class:: netrc([file]) A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc - file. The initialization argument, if present, specifies the file to parse. If - no argument is given, the file :file:`.netrc` in the user's home directory -- - as determined by :func:`os.path.expanduser` -- will be read. Otherwise, + file. The initialization argument, if present, specifies the file to parse. If no + argument is given, it will look for the file path in the `NETRC` environment variable. + If that is not set, it defaults to reading the file :file:`.netrc` in the user's home + directory -- as determined by :func:`os.path.expanduser`. If the file cannot be found, a :exc:`FileNotFoundError` exception will be raised. Parse errors will raise :exc:`NetrcParseError` with diagnostic information including the file name, line number, and terminating token. From c1e3abfa8e017681a040d5bdfb03f861c3b2d4ee Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sat, 21 Jun 2025 23:06:35 +0000 Subject: [PATCH 04/17] fix linting issues --- Lib/test/test_netrc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 080c3b126b6187..849f04e2730e96 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -24,7 +24,7 @@ def generate_netrc(self, content, filename=".netrc", mode=0o600, encoding="utf-8 write_mode = "w" if sys.platform != "cygwin": write_mode += "t" - + netrc_file = os.path.join(self.tmpdir, filename) with open(netrc_file, mode=write_mode, encoding=encoding) as fp: fp.write(textwrap.dedent(content)) @@ -40,7 +40,7 @@ def use_default_netrc_in_home(*args, **kwargs): with NetrcEnvironment() as helper: helper.environ.unset("NETRC") helper.environ.unset("HOME") - + helper.generate_netrc(*args, **kwargs) with mock.patch("os.path.expanduser"): @@ -54,7 +54,7 @@ def use_netrc_envvar(*args, **kwargs): helper.environ.set("NETRC", netrc_file) return netrc.netrc() - + @staticmethod def use_file_argument(*args, **kwargs): with NetrcEnvironment() as helper: @@ -157,7 +157,7 @@ def _test_token_x(self, make_nrc, content, token, value): self.assertEqual(nrc.hosts['host.domain.com'], ('log', value, 'pass')) elif token == 'password': self.assertEqual(nrc.hosts['host.domain.com'], ('log', 'acct', value)) - + @support.subTests('make_nrc', ALL_STRATEGIES) def test_token_value_quotes(self, make_nrc): self._test_token_x(make_nrc, """\ From d877728e09c643ae250ffe6369e1ae3813aabfa9 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sat, 21 Jun 2025 23:10:21 +0000 Subject: [PATCH 05/17] fix doc linting --- Doc/library/netrc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst index 8ae13e815dcf89..defe7f6da7ec9d 100644 --- a/Doc/library/netrc.rst +++ b/Doc/library/netrc.rst @@ -19,7 +19,7 @@ the Unix :program:`ftp` program and other FTP clients. A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc file. The initialization argument, if present, specifies the file to parse. If no - argument is given, it will look for the file path in the `NETRC` environment variable. + argument is given, it will look for the file path in the :envvar:`NETRC` environment variable. If that is not set, it defaults to reading the file :file:`.netrc` in the user's home directory -- as determined by :func:`os.path.expanduser`. If the file cannot be found, a :exc:`FileNotFoundError` exception will be raised. From b46c00a9da4665152474e2a46c45006718fc19eb Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 07:22:20 +0000 Subject: [PATCH 06/17] Add docstrings and fix documentation --- Doc/library/netrc.rst | 2 +- Lib/test/test_netrc.py | 125 ++++++++++++++++++++++++++++------------- 2 files changed, 88 insertions(+), 39 deletions(-) diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst index defe7f6da7ec9d..06cba2bb3e830f 100644 --- a/Doc/library/netrc.rst +++ b/Doc/library/netrc.rst @@ -19,7 +19,7 @@ the Unix :program:`ftp` program and other FTP clients. A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc file. The initialization argument, if present, specifies the file to parse. If no - argument is given, it will look for the file path in the :envvar:`NETRC` environment variable. + argument is given, it will look for the file path in the :envvar:`!NETRC` environment variable. If that is not set, it defaults to reading the file :file:`.netrc` in the user's home directory -- as determined by :func:`os.path.expanduser`. If the file cannot be found, a :exc:`FileNotFoundError` exception will be raised. diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 849f04e2730e96..649a6a972236e3 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -1,7 +1,6 @@ import netrc, os, unittest, sys, textwrap, tempfile from test import support -from unittest import mock from contextlib import ExitStack try: @@ -11,16 +10,43 @@ class NetrcEnvironment: - def __enter__(self): + """ + Context manager for setting up an isolated environment to test `.netrc` file handling. + + This class configures a temporary directory for the `.netrc` file and environment variables, providing + a controlled setup to simulate different scenarios. + """ + + def __enter__(self) -> 'NetrcEnvironment': + """ + Enters the managed environment. + """ self.stack = ExitStack() self.environ = self.stack.enter_context(support.os_helper.EnvironmentVarGuard()) self.tmpdir = self.stack.enter_context(tempfile.TemporaryDirectory()) return self - def __exit__(self, *ignore_exc): + def __exit__(self, *ignore_exc) -> None: + """ + Exits the managed environment and performs cleanup. This method closes the `ExitStack`, + which automatically cleans up the temporary directory and environment. + """ self.stack.close() - def generate_netrc(self, content, filename=".netrc", mode=0o600, encoding="utf-8"): + def generate_netrc(self, content, filename=".netrc", mode=0o600, encoding="utf-8") -> str: + """ + Creates a `.netrc` file in the temporary directory with the given content and permissions. + + Args: + content (str): The content to write into the `.netrc` file. + filename (str, optional): The name of the file to write. Defaults to ".netrc". + mode (int, optional): File permission bits to set after writing. Defaults to `0o600`. Mode + is set only if the platform supports `chmod`. + encoding (str, optional): The encoding used to write the file. Defaults to "utf-8". + + Returns: + str: The full path to the generated `.netrc` file. + """ write_mode = "w" if sys.platform != "cygwin": write_mode += "t" @@ -29,51 +55,74 @@ def generate_netrc(self, content, filename=".netrc", mode=0o600, encoding="utf-8 with open(netrc_file, mode=write_mode, encoding=encoding) as fp: fp.write(textwrap.dedent(content)) - os.chmod(netrc_file, mode=mode) + if support.os_helper.can_chmod(): + os.chmod(netrc_file, mode=mode) return netrc_file class NetrcBuilder: + """ + Utility class to construct and load `netrc.netrc` instances using different configuration scenarios. + + This class provides static methods to simulate different ways the `netrc` module can locate and load + a `.netrc` file. + + These methods are useful for testing or mocking `.netrc` behavior in different system environments. + """ + @staticmethod - def use_default_netrc_in_home(*args, **kwargs): + def use_default_netrc_in_home(*args, **kwargs) -> netrc.netrc: + """ + Loads an instance of netrc using the default `.netrc` file from the user's home directory. + """ with NetrcEnvironment() as helper: helper.environ.unset("NETRC") - helper.environ.unset("HOME") + helper.environ.set("HOME", helper.tmpdir) helper.generate_netrc(*args, **kwargs) - - with mock.patch("os.path.expanduser"): - os.path.expanduser.return_value = helper.tmpdir - return netrc.netrc() + return netrc.netrc() @staticmethod - def use_netrc_envvar(*args, **kwargs): + def use_netrc_envvar(*args, **kwargs) -> netrc.netrc: + """ + Loads an instance of the netrc using the `.netrc` file specified by the `NETRC` environment variable. + """ with NetrcEnvironment() as helper: netrc_file = helper.generate_netrc(*args, **kwargs) - helper.environ.set("NETRC", netrc_file) + return netrc.netrc() @staticmethod - def use_file_argument(*args, **kwargs): + def use_file_argument(*args, **kwargs) -> netrc.netrc: + """ + Loads an instance of `.netrc` file using the file as argument. + """ with NetrcEnvironment() as helper: + # Just to stress a bit more the test scenario, the NETRC envvar will contain + # rubish information which shouldn't be used helper.environ.set("NETRC", "not-a-file.netrc") netrc_file = helper.generate_netrc(*args, **kwargs) return netrc.netrc(netrc_file) @staticmethod - def use_all_strategies(): + def get_all_scenarios(): + """ + Returns all `.netrc` loading scenarios as callables. + + This method is useful for iterating through all supported ways the `.netrc` file can be located. + """ return (NetrcBuilder.use_default_netrc_in_home, NetrcBuilder.use_netrc_envvar, NetrcBuilder.use_file_argument) class NetrcTestCase(unittest.TestCase): - ALL_STRATEGIES = NetrcBuilder.use_all_strategies() + ALL_NETRC_FILE_SCENARIOS = NetrcBuilder.get_all_scenarios() - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_toplevel_non_ordered_tokens(self, make_nrc): nrc = make_nrc("""\ machine host.domain.com password pass1 login log1 account acct1 @@ -82,7 +131,7 @@ def test_toplevel_non_ordered_tokens(self, make_nrc): self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_toplevel_tokens(self, make_nrc): nrc = make_nrc("""\ machine host.domain.com login log1 password pass1 account acct1 @@ -91,7 +140,7 @@ def test_toplevel_tokens(self, make_nrc): self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_macros(self, make_nrc): data = """\ macdef macro1 @@ -110,7 +159,7 @@ def test_macros(self, make_nrc): self.assertRaises(netrc.NetrcParseError, make_nrc, data.rstrip(' ')[:-1]) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_optional_tokens(self, make_nrc): data = ( "machine host.domain.com", @@ -137,7 +186,7 @@ def test_optional_tokens(self, make_nrc): nrc = make_nrc(item) self.assertEqual(nrc.hosts['default'], ('', '', '')) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_invalid_tokens(self, make_nrc): data = ( "invalid host.domain.com", @@ -158,7 +207,7 @@ def _test_token_x(self, make_nrc, content, token, value): elif token == 'password': self.assertEqual(nrc.hosts['host.domain.com'], ('log', 'acct', value)) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_quotes(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login "log" password pass account acct @@ -170,7 +219,7 @@ def test_token_value_quotes(self, make_nrc): machine host.domain.com login log password "pass" account acct """, 'password', 'pass') - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_escape(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login \\"log password pass account acct @@ -191,7 +240,7 @@ def test_token_value_escape(self, make_nrc): machine host.domain.com login log password "\\"pass" account acct """, 'password', '"pass') - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_whitespace(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login "lo g" password pass account acct @@ -203,7 +252,7 @@ def test_token_value_whitespace(self, make_nrc): machine host.domain.com login log password pass account "acc t" """, 'account', 'acc t') - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_non_ascii(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login \xa1\xa2 password pass account acct @@ -215,7 +264,7 @@ def test_token_value_non_ascii(self, make_nrc): machine host.domain.com login log password \xa1\xa2 account acct """, 'password', '\xa1\xa2') - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_leading_hash(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login #log password pass account acct @@ -227,7 +276,7 @@ def test_token_value_leading_hash(self, make_nrc): machine host.domain.com login log password #pass account acct """, 'password', '#pass') - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_trailing_hash(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login log# password pass account acct @@ -239,7 +288,7 @@ def test_token_value_trailing_hash(self, make_nrc): machine host.domain.com login log password pass# account acct """, 'password', 'pass#') - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_internal_hash(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login lo#g password pass account acct @@ -256,7 +305,7 @@ def _test_comment(self, make_nrc, content, passwd='pass'): self.assertEqual(nrc.hosts['foo.domain.com'], ('bar', '', passwd)) self.assertEqual(nrc.hosts['bar.domain.com'], ('foo', '', 'pass')) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_before_machine_line(self, make_nrc): self._test_comment(make_nrc, """\ # comment @@ -264,7 +313,7 @@ def test_comment_before_machine_line(self, make_nrc): machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_before_machine_line_no_space(self, make_nrc): self._test_comment(make_nrc, """\ #comment @@ -272,7 +321,7 @@ def test_comment_before_machine_line_no_space(self, make_nrc): machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_before_machine_line_hash_only(self, make_nrc): self._test_comment(make_nrc, """\ # @@ -280,7 +329,7 @@ def test_comment_before_machine_line_hash_only(self, make_nrc): machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_after_machine_line(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass @@ -293,7 +342,7 @@ def test_comment_after_machine_line(self, make_nrc): # comment """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_after_machine_line_no_space(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass @@ -306,7 +355,7 @@ def test_comment_after_machine_line_no_space(self, make_nrc): #comment """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_after_machine_line_hash_only(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass @@ -319,21 +368,21 @@ def test_comment_after_machine_line_hash_only(self, make_nrc): # """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_at_end_of_machine_line(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass # comment machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_at_end_of_machine_line_no_space(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass #comment machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_at_end_of_machine_line_pass_has_hash(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password #pass #comment @@ -364,7 +413,7 @@ def test_non_anonymous_security(self): @unittest.skipUnless(os.name == 'posix', 'POSIX only test') @unittest.skipIf(pwd is None, 'security check requires pwd module') @support.os_helper.skip_unless_working_chmod - @support.subTests('make_nrc', ALL_STRATEGIES) + @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_anonymous_security(self, make_nrc): # This test is incomplete since we are normally not run as root and # therefore can't test the file ownership being wrong. From 45d4f4ccda71e02f3ee7d77a5626e6ae68f0b09d Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 08:31:43 +0000 Subject: [PATCH 07/17] Wrap lines to 80 chars --- Lib/test/test_netrc.py | 91 ++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 649a6a972236e3..e48758cb4397e8 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -1,4 +1,4 @@ -import netrc, os, unittest, sys, textwrap, tempfile +import netrc, os, unittest, sys, textwrap from test import support from contextlib import ExitStack @@ -11,42 +11,41 @@ class NetrcEnvironment: """ - Context manager for setting up an isolated environment to test `.netrc` file handling. + Context manager for setting up an isolated environment to test `.netrc` file + handling. - This class configures a temporary directory for the `.netrc` file and environment variables, providing - a controlled setup to simulate different scenarios. + This class configures a temporary directory for the `.netrc` file and + environment variables, providing a controlled setup to simulate different + scenarios. """ - def __enter__(self) -> 'NetrcEnvironment': + def __enter__(self): """ - Enters the managed environment. + Enter the managed environment. """ self.stack = ExitStack() - self.environ = self.stack.enter_context(support.os_helper.EnvironmentVarGuard()) - self.tmpdir = self.stack.enter_context(tempfile.TemporaryDirectory()) + self.environ = self.stack.enter_context( + support.os_helper.EnvironmentVarGuard(), + ) + self.tmpdir = self.stack.enter_context(support.os_helper.temp_dir()) return self - def __exit__(self, *ignore_exc) -> None: + def __exit__(self, *ignore_exc): """ - Exits the managed environment and performs cleanup. This method closes the `ExitStack`, - which automatically cleans up the temporary directory and environment. + Exit the managed environment and performs cleanup. This method closes + the `ExitStack`, which automatically cleans up the temporary directory + and environment. """ self.stack.close() - def generate_netrc(self, content, filename=".netrc", mode=0o600, encoding="utf-8") -> str: - """ - Creates a `.netrc` file in the temporary directory with the given content and permissions. - - Args: - content (str): The content to write into the `.netrc` file. - filename (str, optional): The name of the file to write. Defaults to ".netrc". - mode (int, optional): File permission bits to set after writing. Defaults to `0o600`. Mode - is set only if the platform supports `chmod`. - encoding (str, optional): The encoding used to write the file. Defaults to "utf-8". - - Returns: - str: The full path to the generated `.netrc` file. - """ + def generate_netrc( + self, + content, + filename=".netrc", + mode=0o600, + encoding="utf-8", + ): + """Create and return the path to a temporary `.netrc` file.""" write_mode = "w" if sys.platform != "cygwin": write_mode += "t" @@ -62,19 +61,14 @@ def generate_netrc(self, content, filename=".netrc", mode=0o600, encoding="utf-8 class NetrcBuilder: - """ - Utility class to construct and load `netrc.netrc` instances using different configuration scenarios. - - This class provides static methods to simulate different ways the `netrc` module can locate and load - a `.netrc` file. - - These methods are useful for testing or mocking `.netrc` behavior in different system environments. + """Utility class to construct and load `netrc.netrc` instances using + different configuration scenarios. """ @staticmethod - def use_default_netrc_in_home(*args, **kwargs) -> netrc.netrc: - """ - Loads an instance of netrc using the default `.netrc` file from the user's home directory. + def use_default_netrc_in_home(*args, **kwargs): + """Load an instance of netrc using the default `.netrc` file from the + user's home directory. """ with NetrcEnvironment() as helper: helper.environ.unset("NETRC") @@ -84,9 +78,9 @@ def use_default_netrc_in_home(*args, **kwargs) -> netrc.netrc: return netrc.netrc() @staticmethod - def use_netrc_envvar(*args, **kwargs) -> netrc.netrc: - """ - Loads an instance of the netrc using the `.netrc` file specified by the `NETRC` environment variable. + def use_netrc_envvar(*args, **kwargs): + """Load an instance of the netrc using the `.netrc` file specified by + the `NETRC` environment variable. """ with NetrcEnvironment() as helper: netrc_file = helper.generate_netrc(*args, **kwargs) @@ -95,13 +89,12 @@ def use_netrc_envvar(*args, **kwargs) -> netrc.netrc: return netrc.netrc() @staticmethod - def use_file_argument(*args, **kwargs) -> netrc.netrc: - """ - Loads an instance of `.netrc` file using the file as argument. + def use_file_argument(*args, **kwargs): + """Load an instance of `.netrc` file using the file as argument. """ with NetrcEnvironment() as helper: - # Just to stress a bit more the test scenario, the NETRC envvar will contain - # rubish information which shouldn't be used + # Just to stress a bit more the test scenario, the NETRC envvar + # will contain rubish information which shouldn't be used helper.environ.set("NETRC", "not-a-file.netrc") netrc_file = helper.generate_netrc(*args, **kwargs) @@ -109,10 +102,10 @@ def use_file_argument(*args, **kwargs) -> netrc.netrc: @staticmethod def get_all_scenarios(): - """ - Returns all `.netrc` loading scenarios as callables. + """Return all `.netrc` loading scenarios as callables. - This method is useful for iterating through all supported ways the `.netrc` file can be located. + This method is useful for iterating through all supported ways the + `.netrc` file can be located. """ return (NetrcBuilder.use_default_netrc_in_home, NetrcBuilder.use_netrc_envvar, @@ -128,7 +121,8 @@ def test_toplevel_non_ordered_tokens(self, make_nrc): machine host.domain.com password pass1 login log1 account acct1 default login log2 password pass2 account acct2 """) - self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) + self.assertEqual(nrc.hosts['host.domain.com'], + ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) @@ -137,7 +131,8 @@ def test_toplevel_tokens(self, make_nrc): machine host.domain.com login log1 password pass1 account acct1 default login log2 password pass2 account acct2 """) - self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) + self.assertEqual(nrc.hosts['host.domain.com'], + ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) From 33ac518791f877ed8772ea7da63848169d1f4040 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 08:33:29 +0000 Subject: [PATCH 08/17] Fix comments --- Lib/test/test_netrc.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index e48758cb4397e8..82c6ca88680afe 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -10,9 +10,8 @@ class NetrcEnvironment: - """ - Context manager for setting up an isolated environment to test `.netrc` file - handling. + """Context manager for setting up an isolated environment to test + `.netrc` file handling. This class configures a temporary directory for the `.netrc` file and environment variables, providing a controlled setup to simulate different @@ -20,9 +19,7 @@ class NetrcEnvironment: """ def __enter__(self): - """ - Enter the managed environment. - """ + """Enter the managed environment.""" self.stack = ExitStack() self.environ = self.stack.enter_context( support.os_helper.EnvironmentVarGuard(), @@ -31,10 +28,10 @@ def __enter__(self): return self def __exit__(self, *ignore_exc): - """ - Exit the managed environment and performs cleanup. This method closes - the `ExitStack`, which automatically cleans up the temporary directory - and environment. + """Exit the managed environment and performs cleanup. + + This method closes the `ExitStack`, which automatically cleans up the + temporary directory and environment. """ self.stack.close() From 5b9fcfc31d989643c527897ffc6e5c901febb2c7 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 08:37:12 +0000 Subject: [PATCH 09/17] Undo security test --- Lib/test/test_netrc.py | 83 ++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 52 deletions(-) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 82c6ca88680afe..ae2b1714b7c2db 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -381,65 +381,44 @@ def test_comment_at_end_of_machine_line_pass_has_hash(self, make_nrc): machine bar.domain.com login foo password pass """, '#pass') - @unittest.skipUnless(os.name == 'posix', 'POSIX only test') - @unittest.skipIf(pwd is None, 'security check requires pwd module') - @support.os_helper.skip_unless_working_chmod - def test_non_anonymous_security(self): - # This test is incomplete since we are normally not run as root and - # therefore can't test the file ownership being wrong. - content = """ - machine foo.domain.com login bar password pass - default login foo password pass - """ - mode = 0o662 - - # Use ~/.netrc and login is not anon - with self.assertRaises(netrc.NetrcParseError): - NetrcBuilder.use_default_netrc_in_home(content, mode=mode) - - # Don't use default file - nrc = NetrcBuilder.use_file_argument(content, mode=mode) - self.assertEqual(nrc.hosts['foo.domain.com'], - ('bar', '', 'pass')) @unittest.skipUnless(os.name == 'posix', 'POSIX only test') @unittest.skipIf(pwd is None, 'security check requires pwd module') @support.os_helper.skip_unless_working_chmod - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) - def test_anonymous_security(self, make_nrc): + def test_security(self): # This test is incomplete since we are normally not run as root and # therefore can't test the file ownership being wrong. - content = """\ - machine foo.domain.com login anonymous password pass - """ - mode = 0o662 - - # When it's anonymous, file permissions are not bypassed - nrc = make_nrc(content, mode=mode) - self.assertEqual(nrc.hosts['foo.domain.com'], - ('anonymous', '', 'pass')) - - @unittest.skipUnless(os.name == 'posix', 'POSIX only test') - @unittest.skipIf(pwd is None, 'security check requires pwd module') - @support.os_helper.skip_unless_working_chmod - def test_anonymous_security_with_default(self): - # This test is incomplete since we are normally not run as root and - # therefore can't test the file ownership being wrong. - content = """\ - machine foo.domain.com login anonymous password pass - default login foo password pass - """ - mode = 0o622 - - # "foo" is not anonymous, therefore the security check is triggered when we fallback to default netrc - with self.assertRaises(netrc.NetrcParseError): - NetrcBuilder.use_default_netrc_in_home(content, mode=mode) - - # Security check isn't triggered if the file is passed as environment variable or argument - for make_nrc in (NetrcBuilder.use_file_argument, NetrcBuilder.use_netrc_envvar): - nrc = make_nrc(content, mode=mode) + d = support.os_helper.TESTFN + os.mkdir(d) + self.addCleanup(support.os_helper.rmtree, d) + fn = os.path.join(d, '.netrc') + with open(fn, 'wt') as f: + f.write("""\ + machine foo.domain.com login bar password pass + default login foo password pass + """) + with support.os_helper.EnvironmentVarGuard() as environ: + environ.set('HOME', d) + os.chmod(fn, 0o600) + nrc = netrc.netrc() + self.assertEqual(nrc.hosts['foo.domain.com'], + ('bar', '', 'pass')) + os.chmod(fn, 0o622) + self.assertRaises(netrc.NetrcParseError, netrc.netrc) + with open(fn, 'wt') as f: + f.write("""\ + machine foo.domain.com login anonymous password pass + default login foo password pass + """) + with support.os_helper.EnvironmentVarGuard() as environ: + environ.set('HOME', d) + os.chmod(fn, 0o600) + nrc = netrc.netrc() + self.assertEqual(nrc.hosts['foo.domain.com'], + ('anonymous', '', 'pass')) + os.chmod(fn, 0o622) self.assertEqual(nrc.hosts['foo.domain.com'], - ('anonymous', '', 'pass')) + ('anonymous', '', 'pass')) if __name__ == "__main__": From 21ab1735fd057c79450fe75784bee147dfac1a47 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 08:52:28 +0000 Subject: [PATCH 10/17] add versionadded --- Doc/library/netrc.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst index 06cba2bb3e830f..8dbc6b9e7c052c 100644 --- a/Doc/library/netrc.rst +++ b/Doc/library/netrc.rst @@ -46,6 +46,11 @@ the Unix :program:`ftp` program and other FTP clients. can contain arbitrary characters, like whitespace and non-ASCII characters. If the login name is anonymous, it won't trigger the security check. + .. versionadded:: next + :class:`netrc` try to use the value of the :envvar:`NETRC` environment variable + if when *file* is not passed as argument, before falling back to the user's + :file:`.netrc` file in the home directory. + .. exception:: NetrcParseError From 67a464514948062e10f1399396abc9a8e3810611 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 08:59:12 +0000 Subject: [PATCH 11/17] doc change in whatsnew --- Doc/whatsnew/3.15.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f327cf904da1b..6979df57cdfc26 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -116,6 +116,13 @@ math (Contributed by Sergey B Kirpichev in :gh:`132908`.) +netrc +----- + +* Support :envvar:`!NETRC` environment variable in :func:`netrc.netrc`. + (Contributed by Berthin Torres in :gh:`135788`.) + + os.path ------- From 2715bda14b603b326cb3ae17d2bb5f926dba4dd2 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 09:10:15 +0000 Subject: [PATCH 12/17] add news.d entry --- .../Library/2025-06-22-09-04-24.gh-issue-135788.XLT8TW.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-06-22-09-04-24.gh-issue-135788.XLT8TW.rst diff --git a/Misc/NEWS.d/next/Library/2025-06-22-09-04-24.gh-issue-135788.XLT8TW.rst b/Misc/NEWS.d/next/Library/2025-06-22-09-04-24.gh-issue-135788.XLT8TW.rst new file mode 100644 index 00000000000000..5655cd204158b4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-22-09-04-24.gh-issue-135788.XLT8TW.rst @@ -0,0 +1,4 @@ +The :mod:`netrc` module now checks the :envvar:`!NETRC` environment +variable when no file path is explicitly passed to :func:`netrc.netrc`. +If :envvar:`!NETRC` is not set, it falls back to the :file:`.netrc` file in the +user's home directory. From 1ae9bbf4a1f9ce79fdeda9c60919add0716162c3 Mon Sep 17 00:00:00 2001 From: berthin Date: Sun, 22 Jun 2025 11:54:39 +0200 Subject: [PATCH 13/17] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/netrc.rst | 8 +++----- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_netrc.py | 20 ++++---------------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst index 8dbc6b9e7c052c..1ac19d4ae52d80 100644 --- a/Doc/library/netrc.rst +++ b/Doc/library/netrc.rst @@ -19,8 +19,8 @@ the Unix :program:`ftp` program and other FTP clients. A :class:`~netrc.netrc` instance or subclass instance encapsulates data from a netrc file. The initialization argument, if present, specifies the file to parse. If no - argument is given, it will look for the file path in the :envvar:`!NETRC` environment variable. - If that is not set, it defaults to reading the file :file:`.netrc` in the user's home + argument is given, it will look for the file path in the :envvar:`!NETRC` environment variable, + before defaulting to reading the file :file:`.netrc` in the user's home directory -- as determined by :func:`os.path.expanduser`. If the file cannot be found, a :exc:`FileNotFoundError` exception will be raised. Parse errors will raise :exc:`NetrcParseError` with diagnostic @@ -47,9 +47,7 @@ the Unix :program:`ftp` program and other FTP clients. If the login name is anonymous, it won't trigger the security check. .. versionadded:: next - :class:`netrc` try to use the value of the :envvar:`NETRC` environment variable - if when *file* is not passed as argument, before falling back to the user's - :file:`.netrc` file in the home directory. + Added support for the :envvar:`!NETRC` environment variable. .. exception:: NetrcParseError diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6979df57cdfc26..d99a6161dcab2c 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -119,7 +119,7 @@ math netrc ----- -* Support :envvar:`!NETRC` environment variable in :func:`netrc.netrc`. +* Added support for the :envvar:`NETRC` environment variable in :func:`netrc.netrc`. (Contributed by Berthin Torres in :gh:`135788`.) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index ae2b1714b7c2db..a10d10236ce271 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -1,7 +1,7 @@ import netrc, os, unittest, sys, textwrap - -from test import support from contextlib import ExitStack +from test import support +from test.support import os_helper try: import pwd @@ -36,24 +36,15 @@ def __exit__(self, *ignore_exc): self.stack.close() def generate_netrc( - self, - content, - filename=".netrc", - mode=0o600, - encoding="utf-8", + self, content, filename=".netrc", mode=0o600, encoding=None, ): """Create and return the path to a temporary `.netrc` file.""" - write_mode = "w" - if sys.platform != "cygwin": - write_mode += "t" - netrc_file = os.path.join(self.tmpdir, filename) + write_mode = "w" if sys.platform != "cygwin" else "wt" with open(netrc_file, mode=write_mode, encoding=encoding) as fp: fp.write(textwrap.dedent(content)) - if support.os_helper.can_chmod(): os.chmod(netrc_file, mode=mode) - return netrc_file @@ -70,7 +61,6 @@ def use_default_netrc_in_home(*args, **kwargs): with NetrcEnvironment() as helper: helper.environ.unset("NETRC") helper.environ.set("HOME", helper.tmpdir) - helper.generate_netrc(*args, **kwargs) return netrc.netrc() @@ -82,7 +72,6 @@ def use_netrc_envvar(*args, **kwargs): with NetrcEnvironment() as helper: netrc_file = helper.generate_netrc(*args, **kwargs) helper.environ.set("NETRC", netrc_file) - return netrc.netrc() @staticmethod @@ -93,7 +82,6 @@ def use_file_argument(*args, **kwargs): # Just to stress a bit more the test scenario, the NETRC envvar # will contain rubish information which shouldn't be used helper.environ.set("NETRC", "not-a-file.netrc") - netrc_file = helper.generate_netrc(*args, **kwargs) return netrc.netrc(netrc_file) From 2d6021246e0949eee640f0b74bcdb9aa6711279e Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 10:22:45 +0000 Subject: [PATCH 14/17] clean up --- Lib/netrc.py | 8 ++++---- Lib/test/test_netrc.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Lib/netrc.py b/Lib/netrc.py index e78b400d5278ea..c742888b7a209a 100644 --- a/Lib/netrc.py +++ b/Lib/netrc.py @@ -65,10 +65,10 @@ def push_token(self, token): class netrc: def __init__(self, file=None): - netrc_envvar = os.environ.get("NETRC", "") - default_netrc = file is None and not bool(netrc_envvar) - if file is None: - file = netrc_envvar or os.path.join(os.path.expanduser("~"), ".netrc") + envvar_netrc = os.environ.get("NETRC", "") + home_netrc = os.path.join(os.path.expanduser("~"), ".netrc") + file = file or envvar_netrc or home_netrc + default_netrc = (file == home_netrc) self.hosts = {} self.macros = {} try: diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index a10d10236ce271..935fcc6e51967c 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -22,9 +22,9 @@ def __enter__(self): """Enter the managed environment.""" self.stack = ExitStack() self.environ = self.stack.enter_context( - support.os_helper.EnvironmentVarGuard(), + os_helper.EnvironmentVarGuard(), ) - self.tmpdir = self.stack.enter_context(support.os_helper.temp_dir()) + self.tmpdir = self.stack.enter_context(os_helper.temp_dir()) return self def __exit__(self, *ignore_exc): @@ -43,7 +43,7 @@ def generate_netrc( write_mode = "w" if sys.platform != "cygwin" else "wt" with open(netrc_file, mode=write_mode, encoding=encoding) as fp: fp.write(textwrap.dedent(content)) - if support.os_helper.can_chmod(): + if os_helper.can_chmod(): os.chmod(netrc_file, mode=mode) return netrc_file @@ -372,20 +372,20 @@ def test_comment_at_end_of_machine_line_pass_has_hash(self, make_nrc): @unittest.skipUnless(os.name == 'posix', 'POSIX only test') @unittest.skipIf(pwd is None, 'security check requires pwd module') - @support.os_helper.skip_unless_working_chmod + @os_helper.skip_unless_working_chmod def test_security(self): # This test is incomplete since we are normally not run as root and # therefore can't test the file ownership being wrong. - d = support.os_helper.TESTFN + d = os_helper.TESTFN os.mkdir(d) - self.addCleanup(support.os_helper.rmtree, d) + self.addCleanup(os_helper.rmtree, d) fn = os.path.join(d, '.netrc') with open(fn, 'wt') as f: f.write("""\ machine foo.domain.com login bar password pass default login foo password pass """) - with support.os_helper.EnvironmentVarGuard() as environ: + with os_helper.EnvironmentVarGuard() as environ: environ.set('HOME', d) os.chmod(fn, 0o600) nrc = netrc.netrc() @@ -398,7 +398,7 @@ def test_security(self): machine foo.domain.com login anonymous password pass default login foo password pass """) - with support.os_helper.EnvironmentVarGuard() as environ: + with os_helper.EnvironmentVarGuard() as environ: environ.set('HOME', d) os.chmod(fn, 0o600) nrc = netrc.netrc() From c156440990e578fd0a2a9b447ffc1fa20a0bde98 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 10:33:37 +0000 Subject: [PATCH 15/17] fix tests --- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_netrc.py | 51 +++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d99a6161dcab2c..0071c30c991f71 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -119,7 +119,7 @@ math netrc ----- -* Added support for the :envvar:`NETRC` environment variable in :func:`netrc.netrc`. +* Added support for the :envvar:`!NETRC` environment variable in :func:`netrc.netrc`. (Contributed by Berthin Torres in :gh:`135788`.) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 935fcc6e51967c..45e3d9cb03e845 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -1,7 +1,6 @@ -import netrc, os, unittest, sys, textwrap +import netrc, os, unittest, sys, tempfile, textwrap from contextlib import ExitStack -from test import support -from test.support import os_helper +from test.support import os_helper, subTests try: import pwd @@ -24,7 +23,7 @@ def __enter__(self): self.environ = self.stack.enter_context( os_helper.EnvironmentVarGuard(), ) - self.tmpdir = self.stack.enter_context(os_helper.temp_dir()) + self.tmpdir = self.stack.enter_context(tempfile.TemporaryDirectory()) return self def __exit__(self, *ignore_exc): @@ -89,7 +88,7 @@ def use_file_argument(*args, **kwargs): def get_all_scenarios(): """Return all `.netrc` loading scenarios as callables. - This method is useful for iterating through all supported ways the + This method is useful for iterating through all d ways the `.netrc` file can be located. """ return (NetrcBuilder.use_default_netrc_in_home, @@ -100,7 +99,7 @@ def get_all_scenarios(): class NetrcTestCase(unittest.TestCase): ALL_NETRC_FILE_SCENARIOS = NetrcBuilder.get_all_scenarios() - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_toplevel_non_ordered_tokens(self, make_nrc): nrc = make_nrc("""\ machine host.domain.com password pass1 login log1 account acct1 @@ -110,7 +109,7 @@ def test_toplevel_non_ordered_tokens(self, make_nrc): ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_toplevel_tokens(self, make_nrc): nrc = make_nrc("""\ machine host.domain.com login log1 password pass1 account acct1 @@ -120,7 +119,7 @@ def test_toplevel_tokens(self, make_nrc): ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_macros(self, make_nrc): data = """\ macdef macro1 @@ -139,7 +138,7 @@ def test_macros(self, make_nrc): self.assertRaises(netrc.NetrcParseError, make_nrc, data.rstrip(' ')[:-1]) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_optional_tokens(self, make_nrc): data = ( "machine host.domain.com", @@ -166,7 +165,7 @@ def test_optional_tokens(self, make_nrc): nrc = make_nrc(item) self.assertEqual(nrc.hosts['default'], ('', '', '')) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_invalid_tokens(self, make_nrc): data = ( "invalid host.domain.com", @@ -187,7 +186,7 @@ def _test_token_x(self, make_nrc, content, token, value): elif token == 'password': self.assertEqual(nrc.hosts['host.domain.com'], ('log', 'acct', value)) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_quotes(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login "log" password pass account acct @@ -199,7 +198,7 @@ def test_token_value_quotes(self, make_nrc): machine host.domain.com login log password "pass" account acct """, 'password', 'pass') - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_escape(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login \\"log password pass account acct @@ -220,7 +219,7 @@ def test_token_value_escape(self, make_nrc): machine host.domain.com login log password "\\"pass" account acct """, 'password', '"pass') - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_whitespace(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login "lo g" password pass account acct @@ -232,7 +231,7 @@ def test_token_value_whitespace(self, make_nrc): machine host.domain.com login log password pass account "acc t" """, 'account', 'acc t') - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_non_ascii(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login \xa1\xa2 password pass account acct @@ -244,7 +243,7 @@ def test_token_value_non_ascii(self, make_nrc): machine host.domain.com login log password \xa1\xa2 account acct """, 'password', '\xa1\xa2') - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_leading_hash(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login #log password pass account acct @@ -256,7 +255,7 @@ def test_token_value_leading_hash(self, make_nrc): machine host.domain.com login log password #pass account acct """, 'password', '#pass') - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_trailing_hash(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login log# password pass account acct @@ -268,7 +267,7 @@ def test_token_value_trailing_hash(self, make_nrc): machine host.domain.com login log password pass# account acct """, 'password', 'pass#') - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_token_value_internal_hash(self, make_nrc): self._test_token_x(make_nrc, """\ machine host.domain.com login lo#g password pass account acct @@ -285,7 +284,7 @@ def _test_comment(self, make_nrc, content, passwd='pass'): self.assertEqual(nrc.hosts['foo.domain.com'], ('bar', '', passwd)) self.assertEqual(nrc.hosts['bar.domain.com'], ('foo', '', 'pass')) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_before_machine_line(self, make_nrc): self._test_comment(make_nrc, """\ # comment @@ -293,7 +292,7 @@ def test_comment_before_machine_line(self, make_nrc): machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_before_machine_line_no_space(self, make_nrc): self._test_comment(make_nrc, """\ #comment @@ -301,7 +300,7 @@ def test_comment_before_machine_line_no_space(self, make_nrc): machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_before_machine_line_hash_only(self, make_nrc): self._test_comment(make_nrc, """\ # @@ -309,7 +308,7 @@ def test_comment_before_machine_line_hash_only(self, make_nrc): machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_after_machine_line(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass @@ -322,7 +321,7 @@ def test_comment_after_machine_line(self, make_nrc): # comment """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_after_machine_line_no_space(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass @@ -335,7 +334,7 @@ def test_comment_after_machine_line_no_space(self, make_nrc): #comment """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_after_machine_line_hash_only(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass @@ -348,21 +347,21 @@ def test_comment_after_machine_line_hash_only(self, make_nrc): # """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_at_end_of_machine_line(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass # comment machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_at_end_of_machine_line_no_space(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password pass #comment machine bar.domain.com login foo password pass """) - @support.subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) + @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_comment_at_end_of_machine_line_pass_has_hash(self, make_nrc): self._test_comment(make_nrc, """\ machine foo.domain.com login bar password #pass #comment From ca3155cee1d286cd27cebc4435bbe6a8041e63a7 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 12:27:50 +0000 Subject: [PATCH 16/17] try mocking expanduser to fix windows issues --- Lib/test/test_netrc.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 45e3d9cb03e845..c736dad836a249 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -1,6 +1,7 @@ import netrc, os, unittest, sys, tempfile, textwrap from contextlib import ExitStack from test.support import os_helper, subTests +from unittest import mock try: import pwd @@ -58,10 +59,14 @@ def use_default_netrc_in_home(*args, **kwargs): user's home directory. """ with NetrcEnvironment() as helper: + helper.generate_netrc(*args, **kwargs) helper.environ.unset("NETRC") helper.environ.set("HOME", helper.tmpdir) - helper.generate_netrc(*args, **kwargs) - return netrc.netrc() + real_expanduser = os.path.expanduser + with mock.patch("os.path.expanduser") as mock_expanduser: + mock_expanduser.side_effect = lambda arg: helper.tmpdir \ + if arg == "~" else real_expanduser(arg) + return netrc.netrc() @staticmethod def use_netrc_envvar(*args, **kwargs): From b4c22e9279b969be69a5bcd619245269362b4624 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 13:12:25 +0000 Subject: [PATCH 17/17] undo 80 chars, try again with os_helper.temp_dir --- Lib/test/test_netrc.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index c736dad836a249..540d80b4a8a403 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -1,4 +1,4 @@ -import netrc, os, unittest, sys, tempfile, textwrap +import netrc, os, unittest, sys, textwrap from contextlib import ExitStack from test.support import os_helper, subTests from unittest import mock @@ -24,7 +24,7 @@ def __enter__(self): self.environ = self.stack.enter_context( os_helper.EnvironmentVarGuard(), ) - self.tmpdir = self.stack.enter_context(tempfile.TemporaryDirectory()) + self.tmpdir = self.stack.enter_context(os_helper.temp_dir()) return self def __exit__(self, *ignore_exc): @@ -110,8 +110,7 @@ def test_toplevel_non_ordered_tokens(self, make_nrc): machine host.domain.com password pass1 login log1 account acct1 default login log2 password pass2 account acct2 """) - self.assertEqual(nrc.hosts['host.domain.com'], - ('log1', 'acct1', 'pass1')) + self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) @@ -120,8 +119,7 @@ def test_toplevel_tokens(self, make_nrc): machine host.domain.com login log1 password pass1 account acct1 default login log2 password pass2 account acct2 """) - self.assertEqual(nrc.hosts['host.domain.com'], - ('log1', 'acct1', 'pass1')) + self.assertEqual(nrc.hosts['host.domain.com'], ('log1', 'acct1', 'pass1')) self.assertEqual(nrc.hosts['default'], ('log2', 'acct2', 'pass2')) @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS)