From 4c3c39a1ea9a3bfe017e49fce8d94727a96e6932 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sat, 21 Jun 2025 19:21:13 +0000 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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) From 895d707c4b2877ba3dcd8c4b66748ae67c024233 Mon Sep 17 00:00:00 2001 From: Berthin Torres Date: Sun, 22 Jun 2025 20:45:27 +0000 Subject: [PATCH 18/18] Address review comments --- Lib/test/test_netrc.py | 92 ++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 5eda9bee438f92..e7e9359e11644e 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -44,61 +44,55 @@ def generate_netrc( return netrc_file -class NetrcBuilder: - """Utility class to construct and load `netrc.netrc` instances using - different configuration scenarios. +def use_default_netrc_in_home(*args, **kwargs): + """Load an instance of netrc using the default `.netrc` file from the + user's home directory. """ - - @staticmethod - 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.generate_netrc(*args, **kwargs) - helper.environ.unset("NETRC") - helper.environ.set("HOME", helper.tmpdir) - 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): - """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) - helper.environ.set("NETRC", netrc_file) + with NetrcEnvironment() as helper: + helper.generate_netrc(*args, **kwargs) + helper.environ.unset("NETRC") + helper.environ.set("HOME", helper.tmpdir) + with mock.patch("os.path.expanduser") as mock_expanduser: + def fake_expanduser(path): + return helper.tmpdir if path == "~" else os.path.expanduser(path) + mock_expanduser.side_effect = fake_expanduser return netrc.netrc() - @staticmethod - 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 - helper.environ.set("NETRC", "not-a-file.netrc") - netrc_file = helper.generate_netrc(*args, **kwargs) - return netrc.netrc(netrc_file) - - @staticmethod - def get_all_scenarios(): - """Return all `.netrc` loading scenarios as callables. - - This method is useful for iterating through all d ways the - `.netrc` file can be located. - """ - return (NetrcBuilder.use_default_netrc_in_home, - NetrcBuilder.use_netrc_envvar, - NetrcBuilder.use_file_argument) + +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) + helper.environ.set("NETRC", netrc_file) + return netrc.netrc() + + +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 + helper.environ.set("NETRC", "not-a-file.netrc") + netrc_file = helper.generate_netrc(*args, **kwargs) + return netrc.netrc(netrc_file) + + +def get_all_scenarios(): + """Return all `.netrc` loading scenarios as callables. + + This method is useful for iterating through all ways the + `.netrc` file can be located. + """ + return (use_default_netrc_in_home, + use_netrc_envvar, + use_file_argument) class NetrcTestCase(unittest.TestCase): - ALL_NETRC_FILE_SCENARIOS = NetrcBuilder.get_all_scenarios() + ALL_NETRC_FILE_SCENARIOS = get_all_scenarios() @subTests('make_nrc', ALL_NETRC_FILE_SCENARIOS) def test_toplevel_non_ordered_tokens(self, make_nrc): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy