Skip to content

Commit 54a6742

Browse files
committed
Clean up globals defined in Python source, make sure their __del__ is called
1 parent 58bebdf commit 54a6742

File tree

3 files changed

+93
-15
lines changed

3 files changed

+93
-15
lines changed

Src/IronPython/Runtime/PythonContext.cs

+22-11
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public PythonContext(ScriptDomainManager/*!*/ manager, IDictionary<string, objec
271271
try {
272272
HookAssemblyResolve();
273273
} catch (System.Security.SecurityException) {
274-
// We may not have SecurityPermissionFlag.ControlAppDomain.
274+
// We may not have SecurityPermissionFlag.ControlAppDomain.
275275
// If so, we will not look up sys.path for module loads
276276
}
277277
}
@@ -1238,7 +1238,7 @@ private void UnhookAssemblyResolve() {
12381238
try {
12391239
AppDomain.CurrentDomain.AssemblyResolve -= _resolveHolder.AssemblyResolveEvent;
12401240
} catch (System.Security.SecurityException) {
1241-
// We may not have SecurityPermissionFlag.ControlAppDomain.
1241+
// We may not have SecurityPermissionFlag.ControlAppDomain.
12421242
// If so, we will not look up sys.path for module loads
12431243
}
12441244
}
@@ -1295,6 +1295,17 @@ public override void Shutdown() {
12951295
}
12961296
#endif
12971297
}
1298+
1299+
// clean up globals from modules
1300+
foreach (var module in SystemStateModules.Values) {
1301+
if (module is PythonModule pyModule) {
1302+
pyModule.Cleanup(SharedContext);
1303+
}
1304+
}
1305+
1306+
// allow finalizers to run before shutdown
1307+
GC.Collect();
1308+
GC.WaitForPendingFinalizers();
12981309
}
12991310

13001311
// TODO: ExceptionFormatter service
@@ -1569,7 +1580,7 @@ public override TService GetService<TService>(params object[] args) {
15691580

15701581
/// <summary>
15711582
/// Returns (and creates if necessary) the PythonService that is associated with this PythonContext.
1572-
///
1583+
///
15731584
/// The PythonService is used for providing remoted convenience helpers for the DLR hosting APIs.
15741585
/// </summary>
15751586
internal Hosting.PythonService GetPythonService(Microsoft.Scripting.Hosting.ScriptEngine engine) {
@@ -1678,7 +1689,7 @@ private Dictionary<string, Type> CreateBuiltinTable() {
16781689

16791690
if (Environment.OSVersion.Platform == PlatformID.Unix) {
16801691
// we make our nt package show up as a posix package
1681-
// on unix platforms. Because we build on top of the
1692+
// on unix platforms. Because we build on top of the
16821693
// CLI for all file operations we should be good from
16831694
// there, but modules that check for the presence of
16841695
// names (e.g. os) will do the right thing.
@@ -2529,8 +2540,8 @@ private CallSite<Func<CallSite, object, T>> MakeConvertSite<T>(ConversionResultK
25292540

25302541
/// <summary>
25312542
/// Invokes the specified operation on the provided arguments and returns the new resulting value.
2532-
///
2533-
/// operation is usually a value from StandardOperators (standard CLR/DLR operator) or
2543+
///
2544+
/// operation is usually a value from StandardOperators (standard CLR/DLR operator) or
25342545
/// OperatorStrings (a Python specific operator)
25352546
/// </summary>
25362547
internal object Operation(PythonOperationKind operation, object self, object other) {
@@ -2987,13 +2998,13 @@ internal CultureInfo NumericCulture {
29872998
/// <summary>
29882999
/// Sets the current command dispatcher for the Python command line. The previous dispatcher
29893000
/// is returned. Null can be passed to remove the current command dispatcher.
2990-
///
3001+
///
29913002
/// The command dispatcher will be called with a delegate to be executed. The command dispatcher
29923003
/// should invoke the target delegate in the desired context.
2993-
///
3004+
///
29943005
/// A common use for this is to enable running all REPL commands on the UI thread while the REPL
29953006
/// continues to run on a non-UI thread.
2996-
///
3007+
///
29973008
/// The ipy.exe REPL will call into PythonContext.DispatchCommand to dispatch each execution to
29983009
/// the correct thread. Other REPLs can do the same to support this functionality as well.
29993010
/// </summary>
@@ -3094,8 +3105,8 @@ public int Compare(object x, object y) {
30943105
/// <summary>
30953106
/// Gets a function which can be used for comparing two values using the normal
30963107
/// Python semantics.
3097-
///
3098-
/// If type is null then a generic comparison function is returned. If type is
3108+
///
3109+
/// If type is null then a generic comparison function is returned. If type is
30993110
/// not null a comparison function is returned that's used for just that type.
31003111
/// </summary>
31013112
internal IComparer GetComparer(Type type) {

Src/IronPython/Runtime/PythonModule.cs

+30-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections.Generic;
99
using System.Diagnostics;
1010
using System.Dynamic;
11+
using System.Linq;
1112
using System.Runtime.CompilerServices;
1213
using System.Threading;
1314

@@ -30,6 +31,7 @@ namespace IronPython.Runtime {
3031
public class PythonModule : IDynamicMetaObjectProvider, IPythonMembersList {
3132
private readonly PythonDictionary _dict;
3233
private Scope _scope;
34+
private bool _cleanedUp = false;
3335

3436
public PythonModule() {
3537
_dict = new PythonDictionary();
@@ -49,7 +51,7 @@ internal PythonModule(PythonContext context, Scope scope) {
4951

5052
/// <summary>
5153
/// Creates a new PythonModule with the specified dictionary.
52-
///
54+
///
5355
/// Used for creating modules for builtin modules which don't have any code associated with them.
5456
/// </summary>
5557
internal PythonModule(PythonDictionary dict) {
@@ -253,7 +255,7 @@ public DynamicMetaObject GetMember(PythonGetMemberBinder member, DynamicMetaObje
253255

254256
private DynamicMetaObject GetMemberWorker(DynamicMetaObjectBinder binder, DynamicMetaObject codeContext) {
255257
string name = GetGetMemberName(binder);
256-
var tmp = Expression.Variable(typeof(object), "res");
258+
var tmp = Expression.Variable(typeof(object), "res");
257259

258260
return new DynamicMetaObject(
259261
Expression.Block(
@@ -355,7 +357,7 @@ IList<string> IMembersList.GetMemberNames() {
355357
}
356358

357359
#endregion
358-
360+
359361
internal class DebugProxy {
360362
private readonly PythonModule _module;
361363

@@ -375,6 +377,30 @@ public List<ObjectDebugView> Members {
375377
}
376378
}
377379

378-
380+
/// <summary>
381+
/// Cleanup globals so that they could be garbage collected.
382+
/// Note that it cleans python sourced modules only,
383+
/// because C# modules are more fundamental and their globals may be required when other python object's finalizer is executing.
384+
/// </summary>
385+
public void Cleanup(CodeContext context) {
386+
if (_cleanedUp) {
387+
return;
388+
}
389+
390+
_cleanedUp = true;
391+
392+
if (!_dict.ContainsKey("__file__")) {
393+
return; // not from Python source, skip clean up
394+
}
395+
396+
foreach (var key in _dict.Keys.ToList()) {
397+
var obj = _dict[key];
398+
if (obj is PythonModule module) {
399+
module.Cleanup(context);
400+
} else if (key is string name) {
401+
__delattr__(context, name);
402+
}
403+
}
404+
}
379405
}
380406
}

Tests/test_regressions.py

+41
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def test_cp1234(): ...
1717
import os
1818
import sys
1919
import unittest
20+
from subprocess import check_output
2021

2122
from iptest import IronPythonTestCase, is_cli, is_mono, is_netcoreapp, is_posix, run_test, skipUnlessIronPython, stdout_trapper
2223

@@ -1467,4 +1468,44 @@ def get_class_class(cls):
14671468
self.assertEqual(o.get_self_class(), o.get_class())
14681469
self.assertEqual(o.get_class(), o.get_class_class())
14691470

1471+
@unittest.skipIf(is_mono, 'https://github.com/IronLanguages/ironpython3/issues/937')
1472+
def test_ipy3_gh985(self):
1473+
"""https://github.com/IronLanguages/ironpython3/issues/985"""
1474+
code = """
1475+
import sys
1476+
1477+
1478+
class Foo:
1479+
def __init__(self, name):
1480+
self.name = name
1481+
1482+
def __del__(self):
1483+
sys.stdout.write(self.name + '\\n')
1484+
1485+
1486+
def func():
1487+
foo = Foo('Foo 1')
1488+
bar = Foo('Foo 2')
1489+
del bar
1490+
1491+
1492+
func()
1493+
1494+
1495+
foo = Foo('Foo 3')
1496+
foo = Foo('Foo 4')
1497+
"""
1498+
1499+
test_script_name = os.path.join(self.test_dir, 'ipy3_gh985.py')
1500+
f = open(test_script_name, 'w')
1501+
try:
1502+
f.write(code)
1503+
f.close()
1504+
1505+
output = check_output([sys.executable, test_script_name]).decode(sys.stdout.encoding).strip()
1506+
self.assertEqual(set(output.split(os.linesep)), {'Foo 1', 'Foo 2', 'Foo 3', 'Foo 4'})
1507+
finally:
1508+
os.unlink(test_script_name)
1509+
1510+
14701511
run_test(__name__)

0 commit comments

Comments
 (0)