Skip to content

Commit 467d1fd

Browse files
committed
allow excluding public .NET types from being exposed to Python
Introduced PyExportAttribute and handling for it in AssemblyManager
1 parent 72fae73 commit 467d1fd

9 files changed

+76
-17
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1313
- Added function that sets Py_NoSiteFlag to 1.
1414
- Added support for Jetson Nano.
1515
- Added support for __len__ for .NET classes that implement ICollection
16+
- Added `PyExport` attribute to hide .NET types from Python
1617
- Added PythonException.Format method to format exceptions the same as traceback.format_exception
1718
- Added Runtime.None to be able to pass None as parameter into Python from .NET
1819
- Added PyObject.IsNone() to check if a Python object is None in .NET.

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ def build_extension(self, ext):
306306
_config = "{0}Win".format(CONFIG)
307307
_solution_file = "pythonnet.sln"
308308
_custom_define_constants = False
309+
defines.append("NET40")
309310
elif DEVTOOLS == "MsDev15":
310311
_xbuild = '"{0}"'.format(self._find_msbuild_tool_15())
311312
_config = "{0}Win".format(CONFIG)
@@ -316,6 +317,7 @@ def build_extension(self, ext):
316317
_config = "{0}Mono".format(CONFIG)
317318
_solution_file = "pythonnet.sln"
318319
_custom_define_constants = False
320+
defines.append("NET40")
319321
elif DEVTOOLS == "dotnet":
320322
_xbuild = "dotnet msbuild"
321323
_config = "{0}Mono".format(CONFIG)

src/runtime/PyExportAttribute.cs

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Python.Runtime {
2+
using System;
3+
4+
/// <summary>
5+
/// Controls visibility to Python for public .NET type or an entire assembly
6+
/// </summary>
7+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Enum
8+
| AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Assembly,
9+
AllowMultiple = false,
10+
Inherited = false)]
11+
public class PyExportAttribute : Attribute
12+
{
13+
internal readonly bool Export;
14+
public PyExportAttribute(bool export) { this.Export = export; }
15+
}
16+
}

src/runtime/Python.Runtime.csproj

+10-8
Original file line numberDiff line numberDiff line change
@@ -29,46 +29,46 @@
2929
<PlatformTarget>x64</PlatformTarget>
3030
</PropertyGroup>-->
3131
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseMono'">
32-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON2;PYTHON27;UCS4</DefineConstants>
32+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON2;PYTHON27;UCS4</DefineConstants>
3333
<Optimize>true</Optimize>
3434
<DebugType>pdbonly</DebugType>
3535
</PropertyGroup>
3636
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseMonoPY3'">
37-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON38;UCS4</DefineConstants>
37+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON3;PYTHON38;UCS4</DefineConstants>
3838
<Optimize>true</Optimize>
3939
<DebugType>pdbonly</DebugType>
4040
</PropertyGroup>
4141
<PropertyGroup Condition=" '$(Configuration)' == 'DebugMono'">
4242
<DebugSymbols>true</DebugSymbols>
43-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON2;PYTHON27;UCS4;TRACE;DEBUG</DefineConstants>
43+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON2;PYTHON27;UCS4;TRACE;DEBUG</DefineConstants>
4444
<Optimize>false</Optimize>
4545
<DebugType>full</DebugType>
4646
</PropertyGroup>
4747
<PropertyGroup Condition=" '$(Configuration)' == 'DebugMonoPY3'">
4848
<DebugSymbols>true</DebugSymbols>
49-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON38;UCS4;TRACE;DEBUG</DefineConstants>
49+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON3;PYTHON38;UCS4;TRACE;DEBUG</DefineConstants>
5050
<Optimize>false</Optimize>
5151
<DebugType>full</DebugType>
5252
</PropertyGroup>
5353
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseWin'">
54-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON2;PYTHON27;UCS2</DefineConstants>
54+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON2;PYTHON27;UCS2</DefineConstants>
5555
<Optimize>true</Optimize>
5656
<DebugType>pdbonly</DebugType>
5757
</PropertyGroup>
5858
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseWinPY3'">
59-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON38;UCS2</DefineConstants>
59+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON3;PYTHON38;UCS2</DefineConstants>
6060
<Optimize>true</Optimize>
6161
<DebugType>pdbonly</DebugType>
6262
</PropertyGroup>
6363
<PropertyGroup Condition=" '$(Configuration)' == 'DebugWin'">
6464
<DebugSymbols>true</DebugSymbols>
65-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON2;PYTHON27;UCS2;TRACE;DEBUG</DefineConstants>
65+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON2;PYTHON27;UCS2;TRACE;DEBUG</DefineConstants>
6666
<Optimize>false</Optimize>
6767
<DebugType>full</DebugType>
6868
</PropertyGroup>
6969
<PropertyGroup Condition=" '$(Configuration)' == 'DebugWinPY3'">
7070
<DebugSymbols>true</DebugSymbols>
71-
<DefineConstants Condition="'$(DefineConstants)' == ''">PYTHON3;PYTHON38;UCS2;TRACE;DEBUG</DefineConstants>
71+
<DefineConstants Condition="'$(DefineConstants)' == ''">NET40;PYTHON3;PYTHON38;UCS2;TRACE;DEBUG</DefineConstants>
7272
<Optimize>false</Optimize>
7373
<DebugType>full</DebugType>
7474
</PropertyGroup>
@@ -131,6 +131,7 @@
131131
<Compile Include="propertyobject.cs" />
132132
<Compile Include="pyansistring.cs" />
133133
<Compile Include="pydict.cs" />
134+
<Compile Include="PyExportAttribute.cs" />
134135
<Compile Include="pyfloat.cs" />
135136
<Compile Include="pyint.cs" />
136137
<Compile Include="pyiter.cs" />
@@ -151,6 +152,7 @@
151152
<Compile Include="Util.cs" />
152153
<Compile Include="platform\Types.cs" />
153154
<Compile Include="platform\LibraryLoader.cs" />
155+
<Compile Include="polyfill\ReflectionPolifills.cs" />
154156
<Compile Include="slots\mp_length.cs" />
155157
</ItemGroup>
156158
<ItemGroup Condition=" '$(PythonInteropFile)' != '' ">

src/runtime/assemblymanager.cs

+11-5
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ public static bool LoadImplicit(string name, bool warn = true)
346346
/// </summary>
347347
internal static void ScanAssembly(Assembly assembly)
348348
{
349+
if (assembly.GetCustomAttribute<PyExportAttribute>()?.Export == false)
350+
{
351+
return;
352+
}
349353
// A couple of things we want to do here: first, we want to
350354
// gather a list of all of the namespaces contributed to by
351355
// the assembly.
@@ -458,7 +462,7 @@ public static Type LookupType(string qname)
458462
foreach (Assembly assembly in assemblies)
459463
{
460464
Type type = assembly.GetType(qname);
461-
if (type != null)
465+
if (type != null && IsExported(type))
462466
{
463467
return type;
464468
}
@@ -472,33 +476,35 @@ public static Type LookupType(string qname)
472476
/// type.
473477
/// </summary>
474478
public static IEnumerable<Type> LookupTypes(string qualifiedName)
475-
=> assemblies.Select(assembly => assembly.GetType(qualifiedName)).Where(type => type != null);
479+
=> assemblies.Select(assembly => assembly.GetType(qualifiedName)).Where(type => type != null && IsExported(type));
476480

477481
internal static Type[] GetTypes(Assembly a)
478482
{
479483
if (a.IsDynamic)
480484
{
481485
try
482486
{
483-
return a.GetTypes();
487+
return a.GetTypes().Where(IsExported).ToArray();
484488
}
485489
catch (ReflectionTypeLoadException exc)
486490
{
487491
// Return all types that were successfully loaded
488-
return exc.Types.Where(x => x != null).ToArray();
492+
return exc.Types.Where(x => x != null && IsExported(x)).ToArray();
489493
}
490494
}
491495
else
492496
{
493497
try
494498
{
495-
return a.GetExportedTypes();
499+
return a.GetExportedTypes().Where(IsExported).ToArray();
496500
}
497501
catch (FileNotFoundException)
498502
{
499503
return new Type[0];
500504
}
501505
}
502506
}
507+
508+
static bool IsExported(Type type) => type.GetCustomAttribute<PyExportAttribute>()?.Export != false;
503509
}
504510
}
+19-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
using System;
2+
using System.Linq;
23
using System.Reflection;
34
using System.Reflection.Emit;
45

56
namespace Python.Runtime
67
{
7-
#if NETSTANDARD
8+
[Obsolete("This API is for internal use only")]
89
public static class ReflectionPolifills
910
{
11+
#if NETSTANDARD
1012
public static AssemblyBuilder DefineDynamicAssembly(this AppDomain appDomain, AssemblyName assemblyName, AssemblyBuilderAccess assemblyBuilderAccess)
1113
{
1214
return AssemblyBuilder.DefineDynamicAssembly(assemblyName, assemblyBuilderAccess);
@@ -16,6 +18,21 @@ public static Type CreateType(this TypeBuilder typeBuilder)
1618
{
1719
return typeBuilder.GetTypeInfo().GetType();
1820
}
19-
}
2021
#endif
22+
#if NET40
23+
public static T GetCustomAttribute<T>(this Type type) where T: Attribute
24+
{
25+
return type.GetCustomAttributes(typeof(T), inherit: false)
26+
.Cast<T>()
27+
.SingleOrDefault();
28+
}
29+
30+
public static T GetCustomAttribute<T>(this Assembly assembly) where T: Attribute
31+
{
32+
return assembly.GetCustomAttributes(typeof(T), inherit: false)
33+
.Cast<T>()
34+
.SingleOrDefault();
35+
}
36+
#endif
37+
}
2138
}

src/testing/Python.Test.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
<Compile Include="doctest.cs" />
9393
<Compile Include="subclasstest.cs" />
9494
<Compile Include="ReprTest.cs" />
95+
<Compile Include="nonexportable.cs" />
9596
<Compile Include="mp_lengthtest.cs" />
9697
</ItemGroup>
9798
<ItemGroup>

src/testing/nonexportable.cs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Python.Test
2+
{
3+
using Python.Runtime;
4+
5+
// this class should not be visible to Python
6+
[PyExport(false)]
7+
public class NonExportable { }
8+
}

src/tests/test_class.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ def test_basic_reference_type():
1414
"""Test usage of CLR defined reference types."""
1515
assert System.String.Empty == ""
1616

17-
1817
def test_basic_value_type():
1918
"""Test usage of CLR defined value types."""
2019
assert System.Int32.MaxValue == 2147483647
@@ -29,7 +28,6 @@ def test_class_standard_attrs():
2928
assert isinstance(ClassTest.__dict__, DictProxyType)
3029
assert len(ClassTest.__doc__) > 0
3130

32-
3331
def test_class_docstrings():
3432
"""Test standard class docstring generation"""
3533
from Python.Test import ClassTest
@@ -58,6 +56,14 @@ def test_non_public_class():
5856
with pytest.raises(AttributeError):
5957
_ = Test.InternalClass
6058

59+
def test_non_exported():
60+
"""Test [PyExport(false)]"""
61+
with pytest.raises(ImportError):
62+
from Python.Test import NonExportable
63+
64+
with pytest.raises(AttributeError):
65+
_ = Test.NonExportable
66+
6167

6268
def test_basic_subclass():
6369
"""Test basic subclass of a managed class."""

0 commit comments

Comments
 (0)