Repository: pyccel/pyccel
Pull request #2279 (2025-04-12 14:47:14)

Provide context clues to epyccel to access constants and modules
Use `inspect` to provide context clues to epyccel. This gives Pyccel access to the calling context. This is used to insert constants (literal scalars or TypeAliases) and known modules. This will allow us to deprecate the `@template` decorator and stop writing `from numpy import np` in almost all test functions despite the module being available at the top of the file.

**Commit Summary**
- Allow NumPy literals to be used to create `Literal` instances
- Allow a `VariableTypeAnnotation` to be passed to `process_dtype`
- Ensure `Literal` objects contain pure Python literals of the correct type
- Add `context_dict` argument to functions to allow context information to be provided to the semantic parser
- Use evaluated function signature when printing functions in epyccel to ensure types are known
- Rename `function_or_module` in `epyccel` and `epyccel_seq` to reflect the fact that these methods handle classes too
- Use `context_dict` to insert literals, recognised imports and type hints
- Add tests

EmilyBourne: /bot checklist
EmilyBourne: /bot run linux
EmilyBourne: /bot run linux
EmilyBourne: /bot run linux
EmilyBourne: /bot run pylint spelling pyccel_lint docs
EmilyBourne: /bot run pylint docs
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

pyccel-bot[bot]: Hey @pyccel/pyccel-dev ! @EmilyBourne has just created this great new pull request! Check it out and let me know what you think!

pyccel-bot[bot]: @EmilyBourne, @yguclu has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

EmilyBourne: > when do you want to remove import numpy as np from the functions passed to epyccel in the unit tests? Now or in a separate PR?

I don't mind. I didn't want to do it before confirming that this was the right approach but if we are happy with this then I could do it here
pyccel-bot[bot]: Unfortunately your PR is not passing the tests so it is not quite ready for review yet. Let me know when it is fixed with `/bot mark as ready`.

yguclu: > > when do you want to remove import numpy as np from the functions passed to epyccel in the unit tests? Now or in a separate PR?
> 
> I don't mind. I didn't want to do it before confirming that this was the right approach but if we are happy with this then I could do it here

I feel that this PR contains enough changes, so I suggest to leave the `epyccel` unit tests to a separate PR.
yguclu: One point in the PR description reads "Don't cast between float types in `LiteralFloat`"... is that true?
yguclu: /bot run pr_tests

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb95d18a..cec405e0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -79,6 +79,7 @@ All notable changes to this project will be documented in this file.
 -   #337 : Add support for returning tuples from functions.
 -   #2194 : Add support for strings as arguments.
 -   #2192 : Add support for the floor division assignment operator.
+-   #2279 : Allow scalar literals (including Type hints) and recognised modules to be deduced from a function's context.
 -   \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 7210492a..24400fd2 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -399,7 +399,7 @@ from mod import f
 f_fast = epyccel(f)
 ```
 In practice `epyccel` copies the contents of `f` into a temporary Python file in the `__epyccel__` directory.
-As a result it is important that all imports are written inside the function when using `epyccel`.
+Although some basic information (scalar literals, type hints, imports of supported libraries) can be deduced from the context, it is important that all other objects are defined inside the function when using `epyccel`.
 Once the file has been copied, `epyccel` calls the `pyccel` command to generate a Python C extension module that contains a single pyccelised function.
 Then finally, it imports this function and returns it to the caller.
 
diff --git a/pyccel/ast/literals.py b/pyccel/ast/literals.py
index b95967c1..d946f360 100644
--- a/pyccel/ast/literals.py
+++ b/pyccel/ast/literals.py
@@ -4,6 +4,7 @@
 #------------------------------------------------------------------------------------------#
 """ This module contains all literal types
 """
+import numpy as np
 from pyccel.utilities.metaclasses import Singleton
 
 from .basic     import TypedAstNode, PyccelAstNode
@@ -138,9 +139,9 @@ class LiteralInteger(Literal):
 
     def __init__(self, value, dtype = PythonNativeInt()):
         assert value >= 0
-        if not isinstance(value, int):
+        if not isinstance(value, (int, np.integer)):
             raise TypeError("A LiteralInteger can only be created with an integer")
-        self._value = value
+        self._value = int(value)
         self._class_type = dtype
         super().__init__()
 
@@ -174,7 +175,7 @@ class LiteralFloat(Literal):
     __slots__   = ('_value', '_class_type')
 
     def __init__(self, value, dtype = PythonNativeFloat()):
-        if not isinstance(value, (int, float, LiteralFloat)):
+        if not isinstance(value, (int, float, LiteralFloat, np.integer, np.floating)):
             raise TypeError("A LiteralFloat can only be created with an integer or a float")
         if isinstance(value, LiteralFloat):
             self._value = value.python_value
@@ -237,7 +238,8 @@ class LiteralComplex(Literal):
         Extract the Python value from the input argument.
 
         Extract the Python value from the input argument which can either
-        be a literal or a Python variable.
+        be a literal or a Python variable. The input argument represents
+        either the real or the imaginary part of the complex literal.
 
         Parameters
         ----------
@@ -250,9 +252,9 @@ class LiteralComplex(Literal):
             The Python value of the argument.
         """
         if isinstance(arg, Literal):
-            return arg.python_value
-        elif isinstance(arg, (int, float)):
-            return arg
+            return float(arg.python_value)
+        elif isinstance(arg, (int, float, np.integer, np.floating)):
+            return float(arg)
         else:
             raise TypeError(f"LiteralComplex argument must be an int/float/LiteralInt/LiteralFloat not a {type(arg)}")
 
diff --git a/pyccel/ast/numpyext.py b/pyccel/ast/numpyext.py
index c9e56f7b..e4c1a7c0 100644
--- a/pyccel/ast/numpyext.py
+++ b/pyccel/ast/numpyext.py
@@ -38,7 +38,7 @@ from .numpytypes     import NumpyNumericType, NumpyInt8Type, NumpyInt16Type, Num
 from .numpytypes     import NumpyFloat32Type, NumpyFloat64Type, NumpyFloat128Type, NumpyNDArrayType
 from .numpytypes     import NumpyComplex64Type, NumpyComplex128Type, NumpyComplex256Type, numpy_precision_map
 from .operators      import broadcast, PyccelMinus, PyccelDiv, PyccelMul, PyccelAdd
-from .type_annotations import typenames_to_dtypes as dtype_registry
+from .type_annotations import VariableTypeAnnotation, typenames_to_dtypes as dtype_registry
 from .variable       import Variable, Constant, IndexedElement
 
 errors = Errors()
@@ -610,7 +610,7 @@ def process_dtype(dtype):
 
     Parameters
     ----------
-    dtype : PythonType, PyccelFunctionDef, LiteralString, str
+    dtype : PythonType, PyccelFunctionDef, LiteralString, str, VariableTypeAnnotation
         The actual dtype passed to the NumPy function.
 
     Returns
@@ -625,6 +625,8 @@ def process_dtype(dtype):
     TypeError: In the case of unrecognized argument type.
     TypeError: In the case of passed string argument not recognized as valid dtype.
     """
+    if isinstance(dtype, VariableTypeAnnotation):
+        dtype = dtype.class_type
 
     if isinstance(dtype, PythonType):
         if dtype.arg.rank > 0:
diff --git a/pyccel/codegen/pipeline.py b/pyccel/codegen/pipeline.py
index 23ee91ad..b4c7c8e9 100644
--- a/pyccel/codegen/pipeline.py
+++ b/pyccel/codegen/pipeline.py
@@ -58,7 +58,8 @@ def execute_pyccel(fname, *,
                    accelerators    = (),
                    output_name     = None,
                    compiler_export_file = None,
-                   conda_warnings  = 'basic'):
+                   conda_warnings  = 'basic',
+                   context_dict    = None):
     """
     Run Pyccel on the provided code.
 
@@ -113,6 +114,9 @@ def execute_pyccel(fname, *,
         Name of the JSON file to which compiler information is exported. Default is None.
     conda_warnings : str, optional
         Specify the level of Conda warnings to display (choices: off, basic, verbose), Default is 'basic'.
+    context_dict : dict[str, object], optional
+        A dictionary containing any variables that are available in the calling context.
+        This can allow certain constants to be defined outside of the function passed to epyccel.
     """
     start = time.time()
     timers = {}
@@ -209,7 +213,7 @@ def execute_pyccel(fname, *,
     timers["Initialisation"] = start_syntax-start
     # Parse Python file
     try:
-        parser = Parser(pymod_filepath)
+        parser = Parser(pymod_filepath, context_dict = context_dict)
         parser.parse(verbose=verbose)
     except NotImplementedError as error:
         msg = str(error)
diff --git a/pyccel/commands/epyccel.py b/pyccel/commands/epyccel.py
index f96023d9..e5897f62 100644
--- a/pyccel/commands/epyccel.py
+++ b/pyccel/commands/epyccel.py
@@ -8,7 +8,9 @@
 
 import inspect
 import importlib
+import re
 import sys
+from typing import TypeVar
 import os
 
 from filelock import FileLock, Timeout
@@ -18,28 +20,38 @@ from importlib.machinery import ExtensionFileLoader
 
 from pyccel.utilities.strings  import random_string
 from pyccel.codegen.pipeline   import execute_pyccel
-from pyccel.errors.errors      import ErrorsMode, PyccelError
+from pyccel.errors.errors      import ErrorsMode, PyccelError, Errors
 
-__all__ = ['get_source_function', 'epyccel_seq', 'epyccel']
+errors = Errors()
+
+__all__ = ['get_source_code_and_context', 'epyccel_seq', 'epyccel']
 
 
 #==============================================================================
-def get_source_function(func):
+def get_source_code_and_context(func_or_class):
     """
-    Get the source code from a function.
+    Get the source code and context from a function or a class.
 
     Get a string containing the source code of a function from a function
-    object. Excessive indenting is stripped away.
+    object or the source code of a class from a class object. Excessive
+    indenting is stripped away.
+
+    Additionally retrieve the information about the variables that are
+    available in the calling context. This can allow certain constants such as
+    type hints to be defined outside of the function passed to epyccel.
 
     Parameters
     ----------
-    func : Function
-        A Python function.
+    func_or_class : Function | type
+        A Python function or class.
 
     Returns
     -------
-    str
-        A string containing the source code.
+    code : list[str]
+        A list of strings containing the lines of the source code.
+    context_dict : dict[str, object]
+        A dictionary containing any objects defined in the context
+        which may be useful for the function.
 
     Raises
     ------
@@ -47,15 +59,83 @@ def get_source_function(func):
         A type error is raised if the object passed to the function is not
         callable.
     """
-    if not callable(func):
+    if not callable(func_or_class):
         raise TypeError('Expecting a callable function')
 
-    lines, _ = inspect.getsourcelines(func)
+    lines, _ = inspect.getsourcelines(func_or_class)
     # remove indentation if the first line is indented
-    a = lines[0]
-    leading_spaces = len(a) - len(a.lstrip())
-
-    return ''.join(a[leading_spaces:] for a in lines)
+    unindented_line = lines[0]
+    leading_spaces = len(unindented_line) - len(unindented_line.lstrip())
+    lines = [l[leading_spaces:] for l in lines]
+
+    # Strip trailing comments (e.g. pylint disable)
+    commentless_lines = []
+    for l in lines:
+        comment = l.rfind('#')
+        # Avoid # in a string
+        if l[:comment].count("'") % 2 == 0 and l[:comment].count("'") % 2 == 0:
+            l = l[:comment] + '\n'
+        commentless_lines.append(l)
+
+    # Search for methods
+    methods = [(func_or_class.__name__, func_or_class)] if isinstance(func_or_class, FunctionType) else \
+                inspect.getmembers(func_or_class, predicate=inspect.isfunction)
+
+    func_name_regex = re.compile(r'^\s*def\s+([a-zA-Z0-9_]+)\s*\(')
+    func_match = [re.match(func_name_regex, l) for l in lines]
+    prototypes = {m[1]: i for i, m in enumerate(func_match) if m}
+
+    if len(methods) == 0:
+        return ''.join(lines), {}
+
+    # Build context dict with globals
+    _, method0 = methods[0]
+    context_dict = method0.__globals__.copy()
+
+    # Sort the methods in reverse order of appearance to preserve line indices
+    # during treatment of methods
+    methods.sort(key = lambda m: prototypes[m[0]], reverse=True)
+
+    for m_name, m in methods:
+        # Update context dict with closure vars
+        context_dict.update(inspect.getclosurevars(m).nonlocals)
+
+        # Print signature (Python has already executed the line with the prototype
+        # so any variables in the line cannot be deduced from the closure vars or
+        # globals, the only way to get a clean version is to reprint the signature)
+        sig = inspect.signature(m)
+        prototype_idx = prototypes[m_name]
+
+        method_prototype = lines[prototype_idx]
+        indent = len(method_prototype) - len(method_prototype.lstrip())
+
+        # Handle multi-line prototypes
+        end_of_prototype_idx = next(i for i, l in enumerate(commentless_lines[prototype_idx:]) if l.strip().endswith(':'))
+        if end_of_prototype_idx > prototype_idx:
+            lines = lines[:prototype_idx+1] + lines[end_of_prototype_idx+1:]
+
+        method_prototype = ' '*indent + f'def {m_name}{sig}:\n'
+
+        # TypeVar in a signature appear as +T, -T or ~T but the associated variable
+        # T will not be available from the closure vars or globals, we therefore
+        # search for TypeVars, put their definition in the context_dict and use the
+        # variable name (e.g. T) in the signature.
+        for p in sig.parameters.values():
+            annot = p.annotation
+            if isinstance(annot, TypeVar):
+                name = annot.__name__
+                if name in context_dict:
+                    if context_dict[name] != annot:
+                        errors.report("Multiple TypeVars found with the same name",
+                                severity='fatal', line=1)
+                else:
+                    context_dict[name] = annot
+                method_prototype = method_prototype.replace(str(annot), name)
+
+        # Save the updated prototype
+        lines[prototype_idx] = method_prototype
+
+    return ''.join(lines), context_dict
 
 #==============================================================================
 def get_unique_name(prefix, path):
@@ -106,7 +186,7 @@ def get_unique_name(prefix, path):
     return module_name, lock
 
 #==============================================================================
-def epyccel_seq(function_or_module, *,
+def epyccel_seq(function_class_or_module, *,
                 language      = None,
                 compiler      = None,
                 fflags        = None,
@@ -133,7 +213,7 @@ def epyccel_seq(function_or_module, *,
 
     Parameters
     ----------
-    function_or_module : function | module | str
+    function_class_or_module : function | class | module | str
         Python function or module to be accelerated.
         If a string is passed then it is assumed to be the code from a module which
         should be accelerated. The module must be capable of running as a standalone
@@ -186,11 +266,11 @@ def epyccel_seq(function_or_module, *,
     # Store current directory
     base_dirpath = os.getcwd()
 
-    if isinstance(function_or_module, (FunctionType, type, str)):
+    if isinstance(function_class_or_module, (FunctionType, type, str)):
         dirpath = os.getcwd()
 
-    elif isinstance(function_or_module, ModuleType):
-        dirpath = os.path.dirname(function_or_module.__file__)
+    elif isinstance(function_class_or_module, ModuleType):
+        dirpath = os.path.dirname(function_class_or_module.__file__)
 
     # Define working directory 'folder'
     if folder is None:
@@ -202,22 +282,24 @@ def epyccel_seq(function_or_module, *,
     epyccel_dirname = '__epyccel__' + os.environ.get('PYTEST_XDIST_WORKER', '')
     epyccel_dirpath = os.path.join(folder, epyccel_dirname)
 
+    # Define variable for context information
+    context_dict = None
+
     # ... get the module source code
-    if isinstance(function_or_module, (FunctionType, type)):
-        pyfunc = function_or_module
-        code = get_source_function(pyfunc)
+    if isinstance(function_class_or_module, (FunctionType, type)):
+        code, context_dict = get_source_code_and_context(function_class_or_module)
 
         module_name, module_lock = get_unique_name('mod', epyccel_dirpath)
 
-    elif isinstance(function_or_module, ModuleType):
-        pymod = function_or_module
+    elif isinstance(function_class_or_module, ModuleType):
+        pymod = function_class_or_module
         lines = inspect.getsourcelines(pymod)[0]
         code = ''.join(lines)
 
         module_name, module_lock = get_unique_name(pymod.__name__, epyccel_dirpath)
 
-    elif isinstance(function_or_module, str):
-        code = function_or_module
+    elif isinstance(function_class_or_module, str):
+        code = function_class_or_module
 
         module_name, module_lock = get_unique_name('mod', epyccel_dirpath)
 
@@ -257,7 +339,8 @@ def epyccel_seq(function_or_module, *,
                            debug           = debug,
                            accelerators    = accelerators,
                            output_name     = module_name,
-                           conda_warnings  = conda_warnings)
+                           conda_warnings  = conda_warnings,
+                           context_dict    = context_dict)
         finally:
             # Change working directory back to starting point
             os.chdir(base_dirpath)
@@ -279,9 +362,9 @@ def epyccel_seq(function_or_module, *,
             if not isinstance(loader, ExtensionFileLoader):
                 raise ImportError('Could not load shared library')
 
-        # If Python object was function, extract it from module
-        if isinstance(function_or_module, (FunctionType, type)):
-            func = getattr(package, pyfunc.__name__)
+        # If Python object was a function or a class, extract it from module
+        if isinstance(function_class_or_module, (FunctionType, type)):
+            func = getattr(package, function_class_or_module.__name__)
         else:
             func = None
     finally:
@@ -291,7 +374,7 @@ def epyccel_seq(function_or_module, *,
     return package, func
 
 #==============================================================================
-def epyccel( python_function_or_module, **kwargs ):
+def epyccel( python_function_class_or_module, **kwargs ):
     """
     Accelerate Python function or module using Pyccel in "embedded" mode.
 
@@ -301,10 +384,10 @@ def epyccel( python_function_or_module, **kwargs ):
 
     Parameters
     ----------
-    python_function_or_module : function | module | str
+    python_function_class_or_module : function | class | module | str
         Python function or module to be accelerated.
         If a string is passed then it is assumed to be the code from a module which
-        should be accelerated..
+        should be accelerated.
     **kwargs :
         Additional keyword arguments for configuring the compilation and acceleration process.
         Available options are defined in epyccel_seq.
@@ -312,7 +395,7 @@ def epyccel( python_function_or_module, **kwargs ):
     Returns
     -------
     object
-        Accelerated function or module.
+        Accelerated function, class or module.
 
     See Also 
     -------- 
@@ -326,7 +409,7 @@ def epyccel( python_function_or_module, **kwargs ):
     >>> one_f = epyccel(one, language='fortran')
     >>> one_c = epyccel(one, language='c')
     """
-    assert isinstance( python_function_or_module, (FunctionType, type, ModuleType, str) )
+    assert isinstance( python_function_class_or_module, (FunctionType, type, ModuleType, str) )
 
     comm  = kwargs.pop('comm', None)
     root  = kwargs.pop('root', 0)
@@ -356,10 +439,10 @@ def epyccel( python_function_or_module, **kwargs ):
         # Master process calls epyccel
         if comm.rank == root:
             try:
-                mod, fun = epyccel_seq( python_function_or_module, **kwargs )
+                mod, obj = epyccel_seq( python_function_class_or_module, **kwargs )
                 mod_path = os.path.abspath(mod.__file__)
                 mod_name = mod.__name__
-                fun_name = python_function_or_module.__name__ if fun else None
+                obj_name = python_function_class_or_module.__name__ if obj else None
                 success  = True
             # error handling carried out after broadcast to prevent deadlocks
             except PyccelError as e:
@@ -370,10 +453,10 @@ def epyccel( python_function_or_module, **kwargs ):
 
         # Non-master processes initialize empty variables
         else:
-            mod, fun = None, None
+            mod, obj = None, None
             mod_path = None
             mod_name = None
-            fun_name = None
+            obj_name = None
             exc_info = None
             success  = None
 
@@ -385,7 +468,7 @@ def epyccel( python_function_or_module, **kwargs ):
             # Broadcast Fortran module path/name and function name to all processes
             mod_path = comm.bcast( mod_path, root=root )
             mod_name = comm.bcast( mod_name, root=root )
-            fun_name = comm.bcast( fun_name, root=root )
+            obj_name = comm.bcast( obj_name, root=root )
 
             # Non-master processes import Fortran module directly from its path
             # and extract function if its name is given
@@ -397,14 +480,14 @@ def epyccel( python_function_or_module, **kwargs ):
                 importlib.invalidate_caches()
                 mod = importlib.import_module(mod_name)
                 sys.path.remove(folder)
-                fun = getattr(mod, fun_name) if fun_name else None
+                obj = getattr(mod, obj_name) if obj_name else None
 
     # Serial version
     else:
         try:
-            mod, fun = epyccel_seq( python_function_or_module, **kwargs )
+            mod, obj = epyccel_seq( python_function_class_or_module, **kwargs )
         except PyccelError as e:
             raise type(e)(str(e)) from None
 
     # Return Fortran function (if any), otherwise module
-    return fun or mod
+    return obj or mod
diff --git a/pyccel/parser/parser.py b/pyccel/parser/parser.py
index b69092f6..6d96d6f0 100644
--- a/pyccel/parser/parser.py
+++ b/pyccel/parser/parser.py
@@ -27,11 +27,15 @@ class Parser(object):
     filename : str
         The name of the file being translated.
 
+    context_dict : dict, optional
+        A dictionary containing any variables that are available in the calling context.
+        This can allow certain constants to be defined outside of the function passed to epyccel.
+
     **kwargs : dict
         Any keyword arguments for BasicParser.
     """
 
-    def __init__(self, filename, **kwargs):
+    def __init__(self, filename, context_dict = None, **kwargs):
 
         self._filename = filename
         self._kwargs   = kwargs
@@ -48,6 +52,8 @@ class Parser(object):
         self._semantic_parser = None
         self._compile_obj     = None
 
+        self._context_dict = context_dict
+
         self._input_folder = os.path.dirname(filename)
 
     @property
@@ -160,6 +166,22 @@ class Parser(object):
         return parser.ast
 
     def annotate(self, **settings):
+        """
+        Annotate the AST collected from the syntactic stage.
+
+        Use the semantic parser to annotate the AST collected from
+        the syntactic stage.
+
+        Parameters
+        ----------
+        **settings : dict
+            Additional keyword arguments for BasicParser.
+
+        Returns
+        -------
+        SemanticParser
+            The semantic parser that was used to annotate the AST.
+        """
 
         # If the semantic parser already exists, do nothing
         if self._semantic_parser:
@@ -171,8 +193,9 @@ class Parser(object):
 
         # Create a new semantic parser and store it in object
         parser = SemanticParser(self._syntax_parser,
-                                d_parsers=self.d_parsers,
-                                parents=self.parents,
+                                d_parsers = self.d_parsers,
+                                parents = self.parents,
+                                context_dict = self._context_dict,
                                 **settings)
         self._semantic_parser = parser
 
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index ab3d1e2a..ed9b57db 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -9,6 +9,9 @@ See the developer docs for more details
 
 from itertools import chain, product
 import os
+from types import ModuleType
+import sys
+import typing
 import warnings
 
 from sympy.utilities.iterables import iterable as sympy_iterable
@@ -75,6 +78,7 @@ from pyccel.ast.datatypes import PythonNativeBool, PythonNativeInt, PythonNative
 from pyccel.ast.datatypes import DataTypeFactory, HomogeneousContainerType
 from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousTupleType, HomogeneousSetType, HomogeneousListType
 from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType, DictType, TypeAlias
+from pyccel.ast.datatypes import original_type_to_pyccel_type
 
 from pyccel.ast.functionalexpr import FunctionalSum, FunctionalMax, FunctionalMin, GeneratorComprehension, FunctionalFor
 from pyccel.ast.functionalexpr import MaxLimit, MinLimit
@@ -153,6 +157,9 @@ from pyccel.parser.syntax.headers import types_meta
 from pyccel.utilities.stage import PyccelStage
 
 import pyccel.decorators as def_decorators
+
+if sys.version_info >= (3, 10):
+    from types import UnionType
 #==============================================================================
 
 errors = Errors()
@@ -218,11 +225,15 @@ class SemanticParser(BasicParser):
     d_parsers : list
         A list of parsers describing files imported by this file.
 
+    context_dict : dict, optional
+        A dictionary describing any variables in the context where the translated
+        objected was defined.
+
     **kwargs : dict
         Additional keyword arguments for BasicParser.
     """
 
-    def __init__(self, inputs, *, parents = (), d_parsers = (), **kwargs):
+    def __init__(self, inputs, *, parents = (), d_parsers = (), context_dict = None, **kwargs):
 
         # a Parser can have parents, who are importing it.
         # imports are then its sons.
@@ -266,6 +277,9 @@ class SemanticParser(BasicParser):
         # contain persistent pointers
         self._pointer_targets = []
 
+        # provides information about the calling context to collect constants
+        self._context_dict = context_dict or {}
+
         #
         self._code = parser._code
         # ...
@@ -2940,6 +2954,31 @@ class SemanticParser(BasicParser):
             elif name == '*':
                 return GenericType()
 
+        if var is None and name in self._context_dict:
+            env_var = self._context_dict[name]
+            if env_var in original_type_to_pyccel_type:
+                var = VariableTypeAnnotation(original_type_to_pyccel_type[env_var])
+            elif sys.version_info >= (3, 10) and isinstance(env_var, UnionType): # pylint:disable=possibly-used-before-assignment
+                python_types = typing.get_args(env_var)
+                if all(t in original_type_to_pyccel_type for t in python_types):
+                    var = UnionTypeAnnotation(*[VariableTypeAnnotation(original_type_to_pyccel_type[t]) for t in python_types])
+                else:
+                    errors.report(f"Unrecognised type {env_var} found in global scope.",
+                            severity='error', symbol = self.current_ast_node)
+            elif type(env_var) in original_type_to_pyccel_type:
+                var = convert_to_literal(env_var, dtype = original_type_to_pyccel_type[type(env_var)])
+            elif isinstance(env_var, ModuleType):
+                mod_name = env_var.__name__
+                if recognised_source(mod_name):
+                    pyccel_stage.set_stage('syntactic')
+                    import_node = Import(AsName(mod_name, name))
+                    pyccel_stage.set_stage('semantic')
+                    self._additional_exprs[-1].append(self._visit(import_node))
+                    var = self.scope.find(name)
+                else:
+                    errors.report(f"Unrecognised module {mod_name} imported in global scope. Please import the module locally if it was previously Pyccelised.",
+                            severity='error', symbol = self.current_ast_node)
+
         if var is None:
             if name == '_':
                 errors.report(UNDERSCORE_NOT_A_THROWAWAY,
@@ -3457,6 +3496,12 @@ class SemanticParser(BasicParser):
                 name = _get_name(func.name)
                 args = macro.apply(args)
 
+            if func is None and name in self._context_dict:
+                env_var = self._context_dict[name]
+                func = builtin_functions_dict.get(env_var.__name__, None)
+                if func is not None:
+                    func = PyccelFunctionDef(env_var.__name__, func)
+
             if func is None:
                 return errors.report(UNDEFINED_FUNCTION, symbol=name,
                         bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),


Test patch:
diff --git a/tests/epyccel/test_epyccel_classes.py b/tests/epyccel/test_epyccel_classes.py
index 64eae18c..c1a60eba 100644
--- a/tests/epyccel/test_epyccel_classes.py
+++ b/tests/epyccel/test_epyccel_classes.py
@@ -11,6 +11,14 @@ ATOL = 1e-15
 def modnew(language):
     return epyccel(mod, language = language)
 
+def test_empty_class(language):
+    class A:
+        pass
+
+    epyc_A = epyccel(A, language = language)
+
+    assert isinstance(epyc_A, type)
+
 def test_class_import(language):
     class A:
         def __init__(self : 'A'):
diff --git a/tests/epyccel/test_epyccel_context_clues.py b/tests/epyccel/test_epyccel_context_clues.py
new file mode 100644
index 00000000..6e0500cc
--- /dev/null
+++ b/tests/epyccel/test_epyccel_context_clues.py
@@ -0,0 +1,110 @@
+# pylint: disable=missing-function-docstring, missing-class-docstring, missing-module-docstring
+import sys
+from typing import TypeVar
+import numpy as np
+import pytest
+from pyccel import epyccel
+from pyccel.errors.errors import PyccelError
+
+def test_numpy_context(language):
+    def f():
+        a = np.ones(5)
+        return a.sum()
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+
+def test_literal_context(language):
+    a = 4
+    b = np.float32(4.5)
+    def f():
+        return a + b
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()
+    assert isinstance(f(), type(epyc_f()))
+
+def test_type_alias_context(language):
+    T = int
+
+    def f(a : T) -> T:
+        return 2*a
+
+    epyc_f = epyccel(f, language=language)
+    assert f(3) == epyc_f(3)
+    assert isinstance(f(2), type(epyc_f(2)))
+
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="Union of types implemented in Python 3.10")
+def test_type_union_context(language):
+    T = int | float #pylint: disable=unsupported-binary-operation
+
+    def f(a : T):
+        return 2*a
+
+    epyc_f = epyccel(f, language=language)
+    assert f(3) == epyc_f(3)
+    assert isinstance(f(2), type(epyc_f(2)))
+    assert f(3.5) == epyc_f(3.5)
+    assert isinstance(f(2.3), type(epyc_f(2.3)))
+
+def test_type_enclosing_context(language):
+    def get_func(b : bool):
+        T = int if b else float
+
+        def f(a : T):
+            return 2*a
+
+        return f
+
+    epyc_f = epyccel(get_func(True), language=language)
+    py_f = get_func(True)
+    assert py_f(3) == epyc_f(3)
+    assert isinstance(py_f(2), type(epyc_f(2)))
+    epyc_f = epyccel(get_func(False), language=language)
+    py_f = get_func(False)
+    assert py_f(3.5) == epyc_f(3.5)
+    assert isinstance(py_f(2.3), type(epyc_f(2.3)))
+
+def test_bad_type_var_context(language):
+    T = TypeVar('T', int, float)
+    S = TypeVar('T', int, float)
+
+    def f(a : T, b : S) -> T:
+        return 2*a
+
+    with pytest.raises(PyccelError):
+        epyccel(f, language=language)
+
+def test_class_context(language):
+    T = int
+    T2 = float
+    class A:
+        def __init__(self, x : T):
+            self._x : T2 = T2(x)
+
+        def times(self, y : T):
+            return self._x * y
+
+        def __iadd__(self, y : T):
+            self._x += y
+            return self
+
+    epyc_A = epyccel(A, language=language)
+
+    a = A(3)
+    epyc_a = epyc_A(3)
+    assert a.times(2) == epyc_a.times(2)
+    assert isinstance(a.times(2), type(epyc_a.times(2)))
+    a += 5
+    epyc_a += 5
+    assert a.times(1) == epyc_a.times(1)
+    assert isinstance(a.times(1), type(epyc_a.times(1)))
+
+def test_numpy_cast_context(language):
+    T = int
+    def f():
+        a = np.ones(5, dtype=T)
+        return a.sum()
+
+    epyc_f = epyccel(f, language=language)
+    assert f() == epyc_f()