Show More
@@ -1,250 +1,422 b'' | |||
|
1 | # coding=utf-8 | |
|
1 | 2 | from __future__ import absolute_import, print_function |
|
2 | 3 | |
|
3 | 4 | import doctest |
|
4 | 5 | import os |
|
5 | 6 | |
|
6 | 7 | def check(a, b): |
|
7 | 8 | if a != b: |
|
8 | 9 | print((a, b)) |
|
9 | 10 | |
|
10 | 11 | def cert(cn): |
|
11 | 12 | return {'subject': ((('commonName', cn),),)} |
|
12 | 13 | |
|
13 | 14 | from mercurial import ( |
|
14 | 15 | sslutil, |
|
15 | 16 | ) |
|
16 | 17 | |
|
17 | 18 | _verifycert = sslutil._verifycert |
|
18 | 19 | # Test non-wildcard certificates |
|
19 | 20 | check(_verifycert(cert('example.com'), 'example.com'), |
|
20 | 21 | None) |
|
21 | 22 | check(_verifycert(cert('example.com'), 'www.example.com'), |
|
22 | 23 | 'certificate is for example.com') |
|
23 | 24 | check(_verifycert(cert('www.example.com'), 'example.com'), |
|
24 | 25 | 'certificate is for www.example.com') |
|
25 | 26 | |
|
26 | 27 | # Test wildcard certificates |
|
27 | 28 | check(_verifycert(cert('*.example.com'), 'www.example.com'), |
|
28 | 29 | None) |
|
29 | 30 | check(_verifycert(cert('*.example.com'), 'example.com'), |
|
30 | 31 | 'certificate is for *.example.com') |
|
31 | 32 | check(_verifycert(cert('*.example.com'), 'w.w.example.com'), |
|
32 | 33 | 'certificate is for *.example.com') |
|
33 | 34 | |
|
34 | 35 | # Test subjectAltName |
|
35 | 36 | san_cert = {'subject': ((('commonName', 'example.com'),),), |
|
36 | 37 | 'subjectAltName': (('DNS', '*.example.net'), |
|
37 | 38 | ('DNS', 'example.net'))} |
|
38 | 39 | check(_verifycert(san_cert, 'example.net'), |
|
39 | 40 | None) |
|
40 | 41 | check(_verifycert(san_cert, 'foo.example.net'), |
|
41 | 42 | None) |
|
42 | 43 | # no fallback to subject commonName when subjectAltName has DNS |
|
43 | 44 | check(_verifycert(san_cert, 'example.com'), |
|
44 | 45 | 'certificate is for *.example.net, example.net') |
|
45 | 46 | # fallback to subject commonName when no DNS in subjectAltName |
|
46 | 47 | san_cert = {'subject': ((('commonName', 'example.com'),),), |
|
47 | 48 | 'subjectAltName': (('IP Address', '8.8.8.8'),)} |
|
48 | 49 | check(_verifycert(san_cert, 'example.com'), None) |
|
49 | 50 | |
|
50 | 51 | # Avoid some pitfalls |
|
51 | 52 | check(_verifycert(cert('*.foo'), 'foo'), |
|
52 | 53 | 'certificate is for *.foo') |
|
53 | 54 | check(_verifycert(cert('*o'), 'foo'), |
|
54 | 55 | 'certificate is for *o') |
|
55 | 56 | |
|
56 | 57 | check(_verifycert({'subject': ()}, |
|
57 | 58 | 'example.com'), |
|
58 | 59 | 'no commonName or subjectAltName found in certificate') |
|
59 | 60 | check(_verifycert(None, 'example.com'), |
|
60 | 61 | 'no certificate received') |
|
61 | 62 | |
|
62 | 63 | # Unicode (IDN) certname isn't supported |
|
63 | 64 | check(_verifycert(cert(u'\u4f8b.jp'), 'example.jp'), |
|
64 | 65 | 'IDN in certificate not supported') |
|
65 | 66 | |
|
67 | # The following tests are from CPython's test_ssl.py. | |
|
68 | check(_verifycert(cert('example.com'), 'example.com'), None) | |
|
69 | check(_verifycert(cert('example.com'), 'ExAmple.cOm'), None) | |
|
70 | check(_verifycert(cert('example.com'), 'www.example.com'), | |
|
71 | 'certificate is for example.com') | |
|
72 | check(_verifycert(cert('example.com'), '.example.com'), | |
|
73 | 'certificate is for example.com') | |
|
74 | check(_verifycert(cert('example.com'), 'example.org'), | |
|
75 | 'certificate is for example.com') | |
|
76 | check(_verifycert(cert('example.com'), 'exampleXcom'), | |
|
77 | 'certificate is for example.com') | |
|
78 | check(_verifycert(cert('*.a.com'), 'foo.a.com'), None) | |
|
79 | check(_verifycert(cert('*.a.com'), 'bar.foo.a.com'), | |
|
80 | 'certificate is for *.a.com') | |
|
81 | check(_verifycert(cert('*.a.com'), 'a.com'), | |
|
82 | 'certificate is for *.a.com') | |
|
83 | check(_verifycert(cert('*.a.com'), 'Xa.com'), | |
|
84 | 'certificate is for *.a.com') | |
|
85 | check(_verifycert(cert('*.a.com'), '.a.com'), None) | |
|
86 | ||
|
87 | # only match one left-most wildcard | |
|
88 | check(_verifycert(cert('f*.com'), 'foo.com'), | |
|
89 | 'certificate is for f*.com') | |
|
90 | check(_verifycert(cert('f*.com'), 'f.com'), | |
|
91 | 'certificate is for f*.com') | |
|
92 | check(_verifycert(cert('f*.com'), 'bar.com'), | |
|
93 | 'certificate is for f*.com') | |
|
94 | check(_verifycert(cert('f*.com'), 'foo.a.com'), | |
|
95 | 'certificate is for f*.com') | |
|
96 | check(_verifycert(cert('f*.com'), 'bar.foo.com'), | |
|
97 | 'certificate is for f*.com') | |
|
98 | ||
|
99 | # NULL bytes are bad, CVE-2013-4073 | |
|
100 | check(_verifycert(cert('null.python.org\x00example.org'), | |
|
101 | 'null.python.org\x00example.org'), None) | |
|
102 | check(_verifycert(cert('null.python.org\x00example.org'), | |
|
103 | 'example.org'), | |
|
104 | 'certificate is for null.python.org\x00example.org') | |
|
105 | check(_verifycert(cert('null.python.org\x00example.org'), | |
|
106 | 'null.python.org'), | |
|
107 | 'certificate is for null.python.org\x00example.org') | |
|
108 | ||
|
109 | # error cases with wildcards | |
|
110 | check(_verifycert(cert('*.*.a.com'), 'bar.foo.a.com'), | |
|
111 | 'certificate is for *.*.a.com') | |
|
112 | check(_verifycert(cert('*.*.a.com'), 'a.com'), | |
|
113 | 'certificate is for *.*.a.com') | |
|
114 | check(_verifycert(cert('*.*.a.com'), 'Xa.com'), | |
|
115 | 'certificate is for *.*.a.com') | |
|
116 | check(_verifycert(cert('*.*.a.com'), '.a.com'), | |
|
117 | 'certificate is for *.*.a.com') | |
|
118 | ||
|
119 | check(_verifycert(cert('a.*.com'), 'a.foo.com'), | |
|
120 | 'certificate is for a.*.com') | |
|
121 | check(_verifycert(cert('a.*.com'), 'a..com'), | |
|
122 | 'certificate is for a.*.com') | |
|
123 | check(_verifycert(cert('a.*.com'), 'a.com'), | |
|
124 | 'certificate is for a.*.com') | |
|
125 | ||
|
126 | # wildcard doesn't match IDNA prefix 'xn--' | |
|
127 | idna = u'pΓΌthon.python.org'.encode('idna').decode('ascii') | |
|
128 | check(_verifycert(cert(idna), idna), None) | |
|
129 | check(_verifycert(cert('x*.python.org'), idna), | |
|
130 | 'certificate is for x*.python.org') | |
|
131 | check(_verifycert(cert('xn--p*.python.org'), idna), | |
|
132 | 'certificate is for xn--p*.python.org') | |
|
133 | ||
|
134 | # wildcard in first fragment and IDNA A-labels in sequent fragments | |
|
135 | # are supported. | |
|
136 | idna = u'www*.pythΓΆn.org'.encode('idna').decode('ascii') | |
|
137 | check(_verifycert(cert(idna), | |
|
138 | u'www.pythΓΆn.org'.encode('idna').decode('ascii')), | |
|
139 | 'certificate is for www*.xn--pythn-mua.org') | |
|
140 | check(_verifycert(cert(idna), | |
|
141 | u'www1.pythΓΆn.org'.encode('idna').decode('ascii')), | |
|
142 | 'certificate is for www*.xn--pythn-mua.org') | |
|
143 | check(_verifycert(cert(idna), | |
|
144 | u'ftp.pythΓΆn.org'.encode('idna').decode('ascii')), | |
|
145 | 'certificate is for www*.xn--pythn-mua.org') | |
|
146 | check(_verifycert(cert(idna), | |
|
147 | u'pythΓΆn.org'.encode('idna').decode('ascii')), | |
|
148 | 'certificate is for www*.xn--pythn-mua.org') | |
|
149 | ||
|
150 | c = { | |
|
151 | 'notAfter': 'Jun 26 21:41:46 2011 GMT', | |
|
152 | 'subject': (((u'commonName', u'linuxfrz.org'),),), | |
|
153 | 'subjectAltName': ( | |
|
154 | ('DNS', 'linuxfr.org'), | |
|
155 | ('DNS', 'linuxfr.com'), | |
|
156 | ('othername', '<unsupported>'), | |
|
157 | ) | |
|
158 | } | |
|
159 | check(_verifycert(c, 'linuxfr.org'), None) | |
|
160 | check(_verifycert(c, 'linuxfr.com'), None) | |
|
161 | # Not a "DNS" entry | |
|
162 | check(_verifycert(c, '<unsupported>'), | |
|
163 | 'certificate is for linuxfr.org, linuxfr.com') | |
|
164 | # When there is a subjectAltName, commonName isn't used | |
|
165 | check(_verifycert(c, 'linuxfrz.org'), | |
|
166 | 'certificate is for linuxfr.org, linuxfr.com') | |
|
167 | ||
|
168 | # A pristine real-world example | |
|
169 | c = { | |
|
170 | 'notAfter': 'Dec 18 23:59:59 2011 GMT', | |
|
171 | 'subject': ( | |
|
172 | ((u'countryName', u'US'),), | |
|
173 | ((u'stateOrProvinceName', u'California'),), | |
|
174 | ((u'localityName', u'Mountain View'),), | |
|
175 | ((u'organizationName', u'Google Inc'),), | |
|
176 | ((u'commonName', u'mail.google.com'),), | |
|
177 | ), | |
|
178 | } | |
|
179 | check(_verifycert(c, 'mail.google.com'), None) | |
|
180 | check(_verifycert(c, 'gmail.com'), 'certificate is for mail.google.com') | |
|
181 | ||
|
182 | # Only commonName is considered | |
|
183 | check(_verifycert(c, 'California'), 'certificate is for mail.google.com') | |
|
184 | ||
|
185 | # Neither commonName nor subjectAltName | |
|
186 | c = { | |
|
187 | 'notAfter': 'Dec 18 23:59:59 2011 GMT', | |
|
188 | 'subject': ( | |
|
189 | ((u'countryName', u'US'),), | |
|
190 | ((u'stateOrProvinceName', u'California'),), | |
|
191 | ((u'localityName', u'Mountain View'),), | |
|
192 | ((u'organizationName', u'Google Inc'),), | |
|
193 | ), | |
|
194 | } | |
|
195 | check(_verifycert(c, 'mail.google.com'), | |
|
196 | 'no commonName or subjectAltName found in certificate') | |
|
197 | ||
|
198 | # No DNS entry in subjectAltName but a commonName | |
|
199 | c = { | |
|
200 | 'notAfter': 'Dec 18 23:59:59 2099 GMT', | |
|
201 | 'subject': ( | |
|
202 | ((u'countryName', u'US'),), | |
|
203 | ((u'stateOrProvinceName', u'California'),), | |
|
204 | ((u'localityName', u'Mountain View'),), | |
|
205 | ((u'commonName', u'mail.google.com'),), | |
|
206 | ), | |
|
207 | 'subjectAltName': (('othername', 'blabla'),), | |
|
208 | } | |
|
209 | check(_verifycert(c, 'mail.google.com'), None) | |
|
210 | ||
|
211 | # No DNS entry subjectAltName and no commonName | |
|
212 | c = { | |
|
213 | 'notAfter': 'Dec 18 23:59:59 2099 GMT', | |
|
214 | 'subject': ( | |
|
215 | ((u'countryName', u'US'),), | |
|
216 | ((u'stateOrProvinceName', u'California'),), | |
|
217 | ((u'localityName', u'Mountain View'),), | |
|
218 | ((u'organizationName', u'Google Inc'),), | |
|
219 | ), | |
|
220 | 'subjectAltName': (('othername', 'blabla'),), | |
|
221 | } | |
|
222 | check(_verifycert(c, 'google.com'), | |
|
223 | 'no commonName or subjectAltName found in certificate') | |
|
224 | ||
|
225 | # Empty cert / no cert | |
|
226 | check(_verifycert(None, 'example.com'), 'no certificate received') | |
|
227 | check(_verifycert({}, 'example.com'), 'no certificate received') | |
|
228 | ||
|
229 | # avoid denials of service by refusing more than one | |
|
230 | # wildcard per fragment. | |
|
231 | check(_verifycert({'subject': (((u'commonName', u'a*b.com'),),)}, | |
|
232 | 'axxb.com'), 'certificate is for a*b.com') | |
|
233 | check(_verifycert({'subject': (((u'commonName', u'a*b.co*'),),)}, | |
|
234 | 'axxb.com'), 'certificate is for a*b.co*') | |
|
235 | check(_verifycert({'subject': (((u'commonName', u'a*b*.com'),),)}, | |
|
236 | 'axxbxxc.com'), 'certificate is for a*b*.com') | |
|
237 | ||
|
66 | 238 | def test_url(): |
|
67 | 239 | """ |
|
68 | 240 | >>> from mercurial.util import url |
|
69 | 241 | |
|
70 | 242 | This tests for edge cases in url.URL's parsing algorithm. Most of |
|
71 | 243 | these aren't useful for documentation purposes, so they aren't |
|
72 | 244 | part of the class's doc tests. |
|
73 | 245 | |
|
74 | 246 | Query strings and fragments: |
|
75 | 247 | |
|
76 | 248 | >>> url('http://host/a?b#c') |
|
77 | 249 | <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'> |
|
78 | 250 | >>> url('http://host/a?') |
|
79 | 251 | <url scheme: 'http', host: 'host', path: 'a'> |
|
80 | 252 | >>> url('http://host/a#b#c') |
|
81 | 253 | <url scheme: 'http', host: 'host', path: 'a', fragment: 'b#c'> |
|
82 | 254 | >>> url('http://host/a#b?c') |
|
83 | 255 | <url scheme: 'http', host: 'host', path: 'a', fragment: 'b?c'> |
|
84 | 256 | >>> url('http://host/?a#b') |
|
85 | 257 | <url scheme: 'http', host: 'host', path: '', query: 'a', fragment: 'b'> |
|
86 | 258 | >>> url('http://host/?a#b', parsequery=False) |
|
87 | 259 | <url scheme: 'http', host: 'host', path: '?a', fragment: 'b'> |
|
88 | 260 | >>> url('http://host/?a#b', parsefragment=False) |
|
89 | 261 | <url scheme: 'http', host: 'host', path: '', query: 'a#b'> |
|
90 | 262 | >>> url('http://host/?a#b', parsequery=False, parsefragment=False) |
|
91 | 263 | <url scheme: 'http', host: 'host', path: '?a#b'> |
|
92 | 264 | |
|
93 | 265 | IPv6 addresses: |
|
94 | 266 | |
|
95 | 267 | >>> url('ldap://[2001:db8::7]/c=GB?objectClass?one') |
|
96 | 268 | <url scheme: 'ldap', host: '[2001:db8::7]', path: 'c=GB', |
|
97 | 269 | query: 'objectClass?one'> |
|
98 | 270 | >>> url('ldap://joe:xxx@[2001:db8::7]:80/c=GB?objectClass?one') |
|
99 | 271 | <url scheme: 'ldap', user: 'joe', passwd: 'xxx', host: '[2001:db8::7]', |
|
100 | 272 | port: '80', path: 'c=GB', query: 'objectClass?one'> |
|
101 | 273 | |
|
102 | 274 | Missing scheme, host, etc.: |
|
103 | 275 | |
|
104 | 276 | >>> url('://192.0.2.16:80/') |
|
105 | 277 | <url path: '://192.0.2.16:80/'> |
|
106 | 278 | >>> url('https://mercurial-scm.org') |
|
107 | 279 | <url scheme: 'https', host: 'mercurial-scm.org'> |
|
108 | 280 | >>> url('/foo') |
|
109 | 281 | <url path: '/foo'> |
|
110 | 282 | >>> url('bundle:/foo') |
|
111 | 283 | <url scheme: 'bundle', path: '/foo'> |
|
112 | 284 | >>> url('a?b#c') |
|
113 | 285 | <url path: 'a?b', fragment: 'c'> |
|
114 | 286 | >>> url('http://x.com?arg=/foo') |
|
115 | 287 | <url scheme: 'http', host: 'x.com', query: 'arg=/foo'> |
|
116 | 288 | >>> url('http://joe:xxx@/foo') |
|
117 | 289 | <url scheme: 'http', user: 'joe', passwd: 'xxx', path: 'foo'> |
|
118 | 290 | |
|
119 | 291 | Just a scheme and a path: |
|
120 | 292 | |
|
121 | 293 | >>> url('mailto:John.Doe@example.com') |
|
122 | 294 | <url scheme: 'mailto', path: 'John.Doe@example.com'> |
|
123 | 295 | >>> url('a:b:c:d') |
|
124 | 296 | <url path: 'a:b:c:d'> |
|
125 | 297 | >>> url('aa:bb:cc:dd') |
|
126 | 298 | <url scheme: 'aa', path: 'bb:cc:dd'> |
|
127 | 299 | |
|
128 | 300 | SSH examples: |
|
129 | 301 | |
|
130 | 302 | >>> url('ssh://joe@host//home/joe') |
|
131 | 303 | <url scheme: 'ssh', user: 'joe', host: 'host', path: '/home/joe'> |
|
132 | 304 | >>> url('ssh://joe:xxx@host/src') |
|
133 | 305 | <url scheme: 'ssh', user: 'joe', passwd: 'xxx', host: 'host', path: 'src'> |
|
134 | 306 | >>> url('ssh://joe:xxx@host') |
|
135 | 307 | <url scheme: 'ssh', user: 'joe', passwd: 'xxx', host: 'host'> |
|
136 | 308 | >>> url('ssh://joe@host') |
|
137 | 309 | <url scheme: 'ssh', user: 'joe', host: 'host'> |
|
138 | 310 | >>> url('ssh://host') |
|
139 | 311 | <url scheme: 'ssh', host: 'host'> |
|
140 | 312 | >>> url('ssh://') |
|
141 | 313 | <url scheme: 'ssh'> |
|
142 | 314 | >>> url('ssh:') |
|
143 | 315 | <url scheme: 'ssh'> |
|
144 | 316 | |
|
145 | 317 | Non-numeric port: |
|
146 | 318 | |
|
147 | 319 | >>> url('http://example.com:dd') |
|
148 | 320 | <url scheme: 'http', host: 'example.com', port: 'dd'> |
|
149 | 321 | >>> url('ssh://joe:xxx@host:ssh/foo') |
|
150 | 322 | <url scheme: 'ssh', user: 'joe', passwd: 'xxx', host: 'host', port: 'ssh', |
|
151 | 323 | path: 'foo'> |
|
152 | 324 | |
|
153 | 325 | Bad authentication credentials: |
|
154 | 326 | |
|
155 | 327 | >>> url('http://joe@joeville:123@4:@host/a?b#c') |
|
156 | 328 | <url scheme: 'http', user: 'joe@joeville', passwd: '123@4:', |
|
157 | 329 | host: 'host', path: 'a', query: 'b', fragment: 'c'> |
|
158 | 330 | >>> url('http://!*#?/@!*#?/:@host/a?b#c') |
|
159 | 331 | <url scheme: 'http', host: '!*', fragment: '?/@!*#?/:@host/a?b#c'> |
|
160 | 332 | >>> url('http://!*#?@!*#?:@host/a?b#c') |
|
161 | 333 | <url scheme: 'http', host: '!*', fragment: '?@!*#?:@host/a?b#c'> |
|
162 | 334 | >>> url('http://!*@:!*@@host/a?b#c') |
|
163 | 335 | <url scheme: 'http', user: '!*@', passwd: '!*@', host: 'host', |
|
164 | 336 | path: 'a', query: 'b', fragment: 'c'> |
|
165 | 337 | |
|
166 | 338 | File paths: |
|
167 | 339 | |
|
168 | 340 | >>> url('a/b/c/d.g.f') |
|
169 | 341 | <url path: 'a/b/c/d.g.f'> |
|
170 | 342 | >>> url('/x///z/y/') |
|
171 | 343 | <url path: '/x///z/y/'> |
|
172 | 344 | >>> url('/foo:bar') |
|
173 | 345 | <url path: '/foo:bar'> |
|
174 | 346 | >>> url('\\\\foo:bar') |
|
175 | 347 | <url path: '\\\\foo:bar'> |
|
176 | 348 | >>> url('./foo:bar') |
|
177 | 349 | <url path: './foo:bar'> |
|
178 | 350 | |
|
179 | 351 | Non-localhost file URL: |
|
180 | 352 | |
|
181 | 353 | >>> u = url('file://mercurial-scm.org/foo') |
|
182 | 354 | Traceback (most recent call last): |
|
183 | 355 | File "<stdin>", line 1, in ? |
|
184 | 356 | Abort: file:// URLs can only refer to localhost |
|
185 | 357 | |
|
186 | 358 | Empty URL: |
|
187 | 359 | |
|
188 | 360 | >>> u = url('') |
|
189 | 361 | >>> u |
|
190 | 362 | <url path: ''> |
|
191 | 363 | >>> str(u) |
|
192 | 364 | '' |
|
193 | 365 | |
|
194 | 366 | Empty path with query string: |
|
195 | 367 | |
|
196 | 368 | >>> str(url('http://foo/?bar')) |
|
197 | 369 | 'http://foo/?bar' |
|
198 | 370 | |
|
199 | 371 | Invalid path: |
|
200 | 372 | |
|
201 | 373 | >>> u = url('http://foo/bar') |
|
202 | 374 | >>> u.path = 'bar' |
|
203 | 375 | >>> str(u) |
|
204 | 376 | 'http://foo/bar' |
|
205 | 377 | |
|
206 | 378 | >>> u = url('file:/foo/bar/baz') |
|
207 | 379 | >>> u |
|
208 | 380 | <url scheme: 'file', path: '/foo/bar/baz'> |
|
209 | 381 | >>> str(u) |
|
210 | 382 | 'file:///foo/bar/baz' |
|
211 | 383 | >>> u.localpath() |
|
212 | 384 | '/foo/bar/baz' |
|
213 | 385 | |
|
214 | 386 | >>> u = url('file:///foo/bar/baz') |
|
215 | 387 | >>> u |
|
216 | 388 | <url scheme: 'file', path: '/foo/bar/baz'> |
|
217 | 389 | >>> str(u) |
|
218 | 390 | 'file:///foo/bar/baz' |
|
219 | 391 | >>> u.localpath() |
|
220 | 392 | '/foo/bar/baz' |
|
221 | 393 | |
|
222 | 394 | >>> u = url('file:///f:oo/bar/baz') |
|
223 | 395 | >>> u |
|
224 | 396 | <url scheme: 'file', path: 'f:oo/bar/baz'> |
|
225 | 397 | >>> str(u) |
|
226 | 398 | 'file:///f:oo/bar/baz' |
|
227 | 399 | >>> u.localpath() |
|
228 | 400 | 'f:oo/bar/baz' |
|
229 | 401 | |
|
230 | 402 | >>> u = url('file://localhost/f:oo/bar/baz') |
|
231 | 403 | >>> u |
|
232 | 404 | <url scheme: 'file', host: 'localhost', path: 'f:oo/bar/baz'> |
|
233 | 405 | >>> str(u) |
|
234 | 406 | 'file://localhost/f:oo/bar/baz' |
|
235 | 407 | >>> u.localpath() |
|
236 | 408 | 'f:oo/bar/baz' |
|
237 | 409 | |
|
238 | 410 | >>> u = url('file:foo/bar/baz') |
|
239 | 411 | >>> u |
|
240 | 412 | <url scheme: 'file', path: 'foo/bar/baz'> |
|
241 | 413 | >>> str(u) |
|
242 | 414 | 'file:foo/bar/baz' |
|
243 | 415 | >>> u.localpath() |
|
244 | 416 | 'foo/bar/baz' |
|
245 | 417 | """ |
|
246 | 418 | |
|
247 | 419 | if 'TERM' in os.environ: |
|
248 | 420 | del os.environ['TERM'] |
|
249 | 421 | |
|
250 | 422 | doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE) |
General Comments 0
You need to be logged in to leave comments.
Login now