diff --git a/Doc/library/netrc.rst b/Doc/library/netrc.rst --- a/Doc/library/netrc.rst +++ b/Doc/library/netrc.rst @@ -44,23 +44,30 @@ the Unix :program:`ftp` program and othe .. _netrc-objects: netrc Objects ------------- A :class:`netrc` instance has the following methods: -.. method:: netrc.authenticators(host) +.. method:: netrc.authenticators(host[, login]) Return a 3-tuple ``(login, account, password)`` of authenticators for *host*. If the netrc file did not contain an entry for the given host, return the tuple associated with the 'default' entry. If neither matching host nor default entry is available, return ``None``. + If *login* is given, only return a tuple for this login. If no entry in the + :file:`.netrc` file matches the requested combination of *host* and *login*, + return ``None``. + + .. versionchanged:: 3.5 + Added the *login* parameter. + .. method:: netrc.__repr__() Dump the class data as a string in the format of a netrc file. (This discards comments and may reorder the entries.) Instances of :class:`netrc` have public instance variables: diff --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst --- a/Doc/whatsnew/3.5.rst +++ b/Doc/whatsnew/3.5.rst @@ -129,17 +129,23 @@ New Modules .. ----------- * None yet. Improved Modules ================ -* None yet. + +netrc +----- + +The :meth:`~netrc.netrc.authenticators` method now accepts a *login* argument. +This allows to handle multiple entries for a single host. (Contributed by +Jean-Marc Saffroy and Berker Peksag in :issue:`11416`.) Optimizations ============= Major performance enhancements have been added: * None yet. diff --git a/Lib/netrc.py b/Lib/netrc.py --- a/Lib/netrc.py +++ b/Lib/netrc.py @@ -23,16 +23,17 @@ class netrc: def __init__(self, file=None): default_netrc = file is None if file is None: try: file = os.path.join(os.environ['HOME'], ".netrc") except KeyError: raise OSError("Could not find .netrc: $HOME is not set") self.hosts = {} + self.allhosts = {} self.macros = {} with open(file) as fp: self._parse(file, fp, default_netrc) def _parse(self, file, fp, default_netrc): lexer = shlex.shlex(fp) lexer.wordchars += r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" lexer.commenters = lexer.commenters.replace('#', '') @@ -63,23 +64,26 @@ class netrc: continue else: raise NetrcParseError( "bad toplevel token %r" % tt, file, lexer.lineno) # We're looking at start of an entry for a named machine or default. login = '' account = password = None + self.allhosts.setdefault(entryname, []) self.hosts[entryname] = {} while 1: tt = lexer.get_token() if (tt.startswith('#') or tt in {'', 'machine', 'default', 'macdef'}): if password: - self.hosts[entryname] = (login, account, password) + current_entry = (login, account, password) + self.allhosts[entryname].append(current_entry) + self.hosts[entryname] = current_entry lexer.push_token(tt) break else: raise NetrcParseError( "malformed %s entry %s terminated by %s" % (toplevel, entryname, repr(tt)), file, lexer.lineno) elif tt == 'login' or tt == 'user': @@ -108,34 +112,45 @@ class netrc: "~/.netrc access too permissive: access" " permissions must restrict access to only" " the owner", file, lexer.lineno) password = lexer.get_token() else: raise NetrcParseError("bad follower token %r" % tt, file, lexer.lineno) - def authenticators(self, host): - """Return a (user, account, password) tuple for given host.""" + def authenticators(self, host, login=None): + """Return a (user, account, password) tuple for given host. + + When provided, the optional login argument selects a tuple + with a matching user name. + + """ if host in self.hosts: - return self.hosts[host] + if login is not None: + for host_login in self.allhosts[host]: + if host_login[0] == login: + return host_login + return None + else: + return self.hosts[host] elif 'default' in self.hosts: return self.hosts['default'] else: return None def __repr__(self): """Dump the class data in the format of a .netrc file.""" rep = "" - for host in self.hosts.keys(): - attrs = self.hosts[host] - rep = rep + "machine "+ host + "\n\tlogin " + repr(attrs[0]) + "\n" - if attrs[1]: - rep = rep + "account " + repr(attrs[1]) - rep = rep + "\tpassword " + repr(attrs[2]) + "\n" + for host, attrlist in self.allhosts.items(): + for attrs in attrlist: + rep = rep + "machine " + host + "\n\tlogin " + repr(attrs[0]) + "\n" + if attrs[1]: + rep = rep + "account " + repr(attrs[1]) + rep = rep + "\tpassword " + repr(attrs[2]) + "\n" for macro in self.macros.keys(): rep = rep + "macdef " + macro + "\n" for line in self.macros[macro]: rep = rep + line rep = rep + "\n" return rep if __name__ == '__main__': diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -118,13 +118,23 @@ class NetrcTestCase(unittest.TestCase): environ.set('HOME', d) os.chmod(fn, 0o600) nrc = netrc.netrc() self.assertEqual(nrc.hosts['foo.domain.com'], ('bar', None, 'pass')) os.chmod(fn, 0o622) self.assertRaises(netrc.NetrcParseError, netrc.netrc) -def test_main(): - support.run_unittest(NetrcTestCase) + def test_handle_several_accounts_per_host(self): + nrc = self.make_nrc("""\ + machine host.com login foo password foo account foo + machine host.com login bar password bar account bar + """) + self.assertTupleEqual(nrc.hosts['host.com'], ('bar', 'bar', 'bar')) + self.assertTupleEqual(nrc.authenticators('host.com'), + ('bar', 'bar', 'bar')) + self.assertTupleEqual(nrc.authenticators('host.com', 'foo'), + ('foo', 'foo', 'foo')) + self.assertIsNone(nrc.authenticators('host.com', b'foo')) + self.assertIsNone(nrc.authenticators('host.com', lambda: 'foo')) if __name__ == "__main__": - test_main() + unittest.main()