Skip to content

Commit 99abc89

Browse files
MartinBastiPCManticore
authored andcommitted
New warning: shallow copy of os.environ (#1733)
Shallow copy of os.environ doesn't work as people may expect. os.environ is not a dict object but rather a proxy object, so any changes made on the copy may have unexpected effects on os.environ Instead of copy.copy(os.environ) method os.environ.copy() should be used. Message id is: `shallow-copy-environ` See https://bugs.python.org/issue15373 for details. Resolves: #1301
1 parent 9125084 commit 99abc89

File tree

5 files changed

+116
-2
lines changed

5 files changed

+116
-2
lines changed

CONTRIBUTORS.txt

+3
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,6 @@ Order doesn't matter (not that much, at least ;)
145145
* Daniel Miller: contributor.
146146

147147
* Bryce Guinta: contributor
148+
149+
* Martin Bašti: contributor
150+
Added new check for shallow copy of os.environ

ChangeLog

+13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ Pylint's ChangeLog
44
What's New in Pylint 1.8?
55
=========================
66

7+
* New warning `shallow-copy-environ` added
8+
9+
Shallow copy of os.environ doesn't work as people may expect. os.environ
10+
is not a dict object but rather a proxy object, so any changes made
11+
on copy may have unexpected effects on os.environ
12+
13+
Instead of copy.copy(os.environ) method os.environ.copy() should be
14+
used.
15+
16+
See https://bugs.python.org/issue15373 for details.
17+
18+
Close #1301
19+
720
* Do not display no-absolute-import warning multiple times per file.
821

922
* `trailing-comma-tuple` refactor check now extends to assignment with

doc/whatsnew/1.8.rst

+23
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ Summary -- Release highlights
1414
New checkers
1515
============
1616

17+
* A new check was added, ``shallow-copy-environ``.
18+
19+
This warning message is emitted when shallow copy of os.environ is created.
20+
Shallow copy of os.environ doesn't work as people may expect. os.environ
21+
is not a dict object but rather a proxy object, so any changes made
22+
on copy may have unexpected effects on os.environ
23+
24+
Instead of copy.copy(os.environ) method os.environ.copy() should be used.
25+
26+
See https://bugs.python.org/issue15373 for details.
27+
28+
.. code-block:: python
29+
30+
import copy
31+
import os
32+
wrong_env_copy = copy.copy(os.environ) # will emit pylint warning
33+
wrong_env_copy['ENV_VAR'] = 'new_value' # changes os.environ
34+
assert os.environ['ENV_VAR'] == 'new_value'
35+
36+
good_env_copy = os.environ.copy() # the right way
37+
good_env_copy['ENV_VAR'] = 'different_value' # doesn't change os.environ
38+
assert os.environ['ENV_VAR'] == 'new_value'
39+
1740
* A new check was added, ``keyword-arg-before-vararg``.
1841

1942
This warning message is emitted when a function is defined with a keyword

pylint/checkers/stdlib.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
OPEN_FILES = {'open', 'file'}
2424
UNITTEST_CASE = 'unittest.case'
2525
THREADING_THREAD = 'threading.Thread'
26+
COPY_COPY = 'copy.copy'
27+
OS_ENVIRON = 'os._Environ'
2628
if sys.version_info >= (3, 0):
2729
OPEN_MODULE = '_io'
2830
else:
@@ -105,6 +107,12 @@ class StdlibChecker(BaseChecker):
105107
'The warning is emitted when a threading.Thread class '
106108
'is instantiated without the target function being passed. '
107109
'By default, the first parameter is the group param, not the target param. '),
110+
'W1507': ('Using copy.copy(os.environ). Use os.environ.copy() ' # TODO: number
111+
'instead. ',
112+
'shallow-copy-environ',
113+
'os.environ is not a dict object but proxy object, so '
114+
'shallow copy has still effects on original object. '
115+
'See https://bugs.python.org/issue15373 for reference. '),
108116
}
109117

110118
deprecated = {
@@ -186,9 +194,17 @@ def _check_bad_thread_instantiation(self, node):
186194
if not node.kwargs and node.args:
187195
self.add_message('bad-thread-instantiation', node=node)
188196

197+
def _check_shallow_copy_environ(self, node):
198+
arg = utils.get_argument_from_call(node, position=0)
199+
for inferred in arg.inferred():
200+
if inferred.qname() == OS_ENVIRON:
201+
self.add_message('shallow-copy-environ', node=node)
202+
break
203+
189204
@utils.check_messages('bad-open-mode', 'redundant-unittest-assert',
190205
'deprecated-method',
191-
'bad-thread-instantiation')
206+
'bad-thread-instantiation',
207+
'shallow-copy-environ')
192208
def visit_call(self, node):
193209
"""Visit a Call node."""
194210
try:
@@ -202,6 +218,8 @@ def visit_call(self, node):
202218
self._check_redundant_assert(node, inferred)
203219
if isinstance(inferred, astroid.ClassDef) and inferred.qname() == THREADING_THREAD:
204220
self._check_bad_thread_instantiation(node)
221+
if isinstance(inferred, astroid.FunctionDef) and inferred.qname() == COPY_COPY:
222+
self._check_shallow_copy_environ(node)
205223
self._check_deprecated_method(node, inferred)
206224
except astroid.InferenceError:
207225
return

pylint/test/unittest_checker_stdlib.py

+58-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import astroid
1010

1111
from pylint.checkers import stdlib
12-
from pylint.testutils import CheckerTestCase
12+
from pylint.testutils import CheckerTestCase, Message
13+
from pylint.interfaces import UNDEFINED
1314

1415

1516
@contextlib.contextmanager
@@ -46,3 +47,59 @@ def infer_func(node, context=None):
4647
''')
4748
with self.assertNoMessages():
4849
self.checker.visit_call(node)
50+
51+
def test_copy_environ(self):
52+
# shallow copy of os.environ should be reported
53+
node = astroid.extract_node("""
54+
import copy, os
55+
copy.copy(os.environ)
56+
""")
57+
with self.assertAddsMessages(
58+
Message(
59+
msg_id='shallow-copy-environ', node=node, confidence=UNDEFINED)
60+
):
61+
self.checker.visit_call(node)
62+
63+
def test_copy_environ_hidden(self):
64+
# shallow copy of os.environ should be reported
65+
# hide function names to be sure that checker is not just matching text
66+
node = astroid.extract_node("""
67+
from copy import copy as test_cp
68+
import os as o
69+
test_cp(o.environ)
70+
""")
71+
with self.assertAddsMessages(
72+
Message(
73+
msg_id='shallow-copy-environ', node=node, confidence=UNDEFINED)
74+
):
75+
self.checker.visit_call(node)
76+
77+
def test_copy_dict(self):
78+
# copy of dict is OK
79+
node = astroid.extract_node("""
80+
import copy
81+
test_dict = {}
82+
copy.copy(test_dict)
83+
""")
84+
with self.assertNoMessages():
85+
self.checker.visit_call(node)
86+
87+
def test_copy_uninferable(self):
88+
# copy of uninferable object should not raise exception, nor make
89+
# the checker crash
90+
node = astroid.extract_node("""
91+
import copy
92+
from missing_library import MissingObject
93+
copy.copy(MissingObject)
94+
""")
95+
with self.assertNoMessages():
96+
self.checker.visit_call(node)
97+
98+
def test_deepcopy_environ(self):
99+
# deepcopy of os.environ is OK
100+
node = astroid.extract_node("""
101+
import copy, os
102+
copy.deepcopy(os.environ)
103+
""")
104+
with self.assertNoMessages():
105+
self.checker.visit_call(node)

0 commit comments

Comments
 (0)