├── .devcontainer ├── dockerfile └── devcontainer.json ├── QuantConnectStubsGenerator ├── QuantConnectStubsGenerator.csproj ├── log4net.config ├── Model │ ├── CodeEntity.cs │ ├── Property.cs │ ├── ParseContext.cs │ ├── Parameter.cs │ ├── Namespace.cs │ ├── Method.cs │ ├── Class.cs │ └── PythonType.cs ├── Renderer │ ├── BaseRenderer.cs │ ├── AlgorithmImportsRenderer.cs │ ├── ClrStubsRenderer.cs │ ├── PyLoaderRenderer.cs │ ├── ObjectRenderer.cs │ ├── ClassRenderer.cs │ ├── NamespaceRenderer.cs │ ├── PropertyRenderer.cs │ ├── SetupRenderer.cs │ └── MethodRenderer.cs ├── Program.cs ├── Utility │ ├── StringExtensions.cs │ ├── Extensions.cs │ ├── DependencyGraph.cs │ └── XmlExtensions.cs └── Parser │ ├── ClassParser.cs │ ├── PropertyParser.cs │ ├── TypeConverter.cs │ └── BaseParser.cs ├── QuantConnectStubsGenerator.Tests ├── QuantConnectStubsGenerator.Tests.csproj ├── Utility │ ├── StringExtensionsTests.cs │ └── DependencyGraphTests.cs ├── Model │ ├── ParseContextTests.cs │ ├── PythonTypeTests.cs │ ├── NamespaceTests.cs │ └── ClassTests.cs ├── MethodParserTests.cs └── GeneratorTests.cs ├── .github └── workflows │ └── build.yml ├── .vscode ├── launch.json └── tasks.json ├── QuantConnectStubsGenerator.sln ├── integration ├── README.md ├── pull_repos.py ├── utils.py └── integration_tests.py ├── README.md ├── .gitignore └── LICENSE /.devcontainer/dockerfile: -------------------------------------------------------------------------------- 1 | # Use Lean Foundation as the base 2 | FROM node:14.19-bullseye 3 | 4 | # Dotnet install 5 | RUN wget https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -O packages-microsoft-prod.deb && \ 6 | dpkg -i packages-microsoft-prod.deb && \ 7 | rm packages-microsoft-prod.deb 8 | 9 | RUN apt-get update; \ 10 | apt-get install -y apt-transport-https && \ 11 | apt-get update && \ 12 | apt-get install -y dotnet-sdk-5.0 13 | 14 | # pip 15 | RUN wget -q https://bootstrap.pypa.io/pip/3.6/get-pip.py \ 16 | && python3 get-pip.py && \ 17 | pip install pandas matplotlib 18 | 19 | # pyright 20 | RUN npm i -g pyright -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/QuantConnectStubsGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net9.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/QuantConnectStubsGenerator.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0 4 | false 5 | 6 | 7 | 8 | 9 | all 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StubsGenerator", 3 | 4 | // Use devcontainer Dockerfile that is based on Lean foundation image 5 | "build": { 6 | "dockerfile": "dockerfile" 7 | }, 8 | 9 | // Add the IDs of extensions you want installed when the container is created. 10 | "extensions": [ 11 | "ms-dotnettools.csharp", 12 | "formulahendry.dotnet-test-explorer", 13 | "ms-python.python", 14 | "ms-python.vscode-pylance", 15 | "eamodio.gitlens", 16 | "yzhang.markdown-all-in-one", 17 | "ms-azuretools.vscode-docker", 18 | "mads-hartmann.bash-ide-vscode", 19 | "rogalmic.bash-debug" 20 | ], 21 | 22 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 23 | // "forwardPorts": [], 24 | 25 | // Post create commands; only runs on initial creation of container 26 | "postCreateCommand": "cd integration && python3 ./pull_repos.py", 27 | 28 | // Post start commands; runs each time the container is started 29 | "postStartCommand": "", 30 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Set up .NET 9 14 | uses: actions/setup-dotnet@v2 15 | with: 16 | dotnet-version: '9.0.x' 17 | 18 | - name: Build 19 | run: dotnet build 20 | 21 | - name: Run unit tests 22 | run: dotnet test -v n 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: 3.11 28 | 29 | - name: Set up Node.js 30 | uses: actions/setup-node@v1 31 | with: 32 | node-version: 14.x 33 | 34 | - name: Install integration test dependencies 35 | run: | 36 | pip install pandas matplotlib pyright==1.1.338 mypy==1.15.0 wheel 37 | 38 | - name: Run integration tests 39 | run: python integration/integration_tests.py 40 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/log4net.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/CodeEntity.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Xml; 17 | 18 | namespace QuantConnectStubsGenerator.Model 19 | { 20 | public enum CodeEntityType 21 | { 22 | Class, 23 | Method, 24 | Property, 25 | Field 26 | } 27 | 28 | public abstract class CodeEntity 29 | { 30 | public string Summary { get; set; } 31 | 32 | public XmlDocument Documentation { get; set; } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Stubs Generator", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceFolder}/QuantConnectStubsGenerator/bin/Debug/net5.0/QuantConnectStubsGenerator.dll", 10 | "args": [ 11 | // Requires Lean and Runtime repos under workspace dir ./generated 12 | "${workspaceFolder}/generated/Lean", 13 | "${workspaceFolder}/generated/runtime", 14 | "${workspaceFolder}/generated/stubs" 15 | ], 16 | "cwd": "${workspaceFolder}/QuantConnectStubsGenerator", 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": "Python Integration Tests", 22 | "type": "python", 23 | "request": "launch", 24 | "program": "${workspaceFolder}/integration/integration_tests.py", 25 | "console": "integratedTerminal" 26 | }, 27 | { 28 | "name": "Python: Current File", 29 | "type": "python", 30 | "request": "launch", 31 | "program": "${file}", 32 | "console": "integratedTerminal" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/BaseRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.IO; 17 | 18 | namespace QuantConnectStubsGenerator.Renderer 19 | { 20 | public abstract class BaseRenderer 21 | { 22 | protected readonly TextWriter Writer; 23 | 24 | protected BaseRenderer(TextWriter writer) 25 | { 26 | Writer = writer; 27 | } 28 | 29 | protected virtual void Write(string value) 30 | { 31 | Writer.Write(value); 32 | } 33 | 34 | protected virtual void WriteLine(string value = "") 35 | { 36 | Writer.WriteLine(value); 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/AlgorithmImportsRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.IO; 17 | 18 | namespace QuantConnectStubsGenerator.Renderer 19 | { 20 | public class AlgorithmImportsRenderer : BaseRenderer 21 | { 22 | private readonly string _leanPath; 23 | 24 | public AlgorithmImportsRenderer(TextWriter writer, string leanPath) : base(writer) 25 | { 26 | _leanPath = leanPath; 27 | } 28 | 29 | public void Render() 30 | { 31 | var algorithmImports = Path.GetFullPath("Common/AlgorithmImports.py", _leanPath); 32 | WriteLine(File.ReadAllText(algorithmImports)); 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/QuantConnectStubsGenerator/QuantConnectStubsGenerator.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/QuantConnectStubsGenerator/QuantConnectStubsGenerator.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/QuantConnectStubsGenerator/QuantConnectStubsGenerator.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnectStubsGenerator", "QuantConnectStubsGenerator\QuantConnectStubsGenerator.csproj", "{61A6CF0C-1044-4787-9ECB-7CF00B5DAF8C}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantConnectStubsGenerator.Tests", "QuantConnectStubsGenerator.Tests\QuantConnectStubsGenerator.Tests.csproj", "{C9BD53DA-57D8-4096-85D5-122224A055F5}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {61A6CF0C-1044-4787-9ECB-7CF00B5DAF8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {61A6CF0C-1044-4787-9ECB-7CF00B5DAF8C}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {61A6CF0C-1044-4787-9ECB-7CF00B5DAF8C}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {61A6CF0C-1044-4787-9ECB-7CF00B5DAF8C}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {C9BD53DA-57D8-4096-85D5-122224A055F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {C9BD53DA-57D8-4096-85D5-122224A055F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {C9BD53DA-57D8-4096-85D5-122224A055F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {C9BD53DA-57D8-4096-85D5-122224A055F5}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/Utility/StringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using NUnit.Framework; 17 | using QuantConnectStubsGenerator.Utility; 18 | 19 | namespace QuantConnectStubsGenerator.Tests.Utility 20 | { 21 | [TestFixture] 22 | public class StringExtensionsTests 23 | { 24 | [Test] 25 | public void IndentShouldIndentByFourTimesTheLevelAmountOfSpaces() 26 | { 27 | Assert.AreEqual(" # Documentation", "# Documentation".Indent(2)); 28 | } 29 | 30 | [Test] 31 | public void IndentShouldIndentAllLines() 32 | { 33 | Assert.AreEqual(" # Line 1\n # Line 2", "# Line 1\n# Line 2".Indent(2)); 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests 2 | 3 | This directory contains the integration tests. These are written in Python because the NUnit runners don't seem to consistently log both external process output and `Console.WriteLine` messages, making debugging a lot harder. 4 | 5 | Please note that these integration tests are not meant to show no errors/warnings at all. Because of the differences between C# and Python, it is sometimes necessary to perform conversions which are invalid according to common PEPs, as long as editors can still read the stubs and provide autocomplete and the like properly. 6 | 7 | ## Usage 8 | 9 | Before running `integration_tests.py`, make sure `pandas` and `matplotlib` are installed in your Python environment and the following commands are available on your `PATH`: 10 | - `git`, to clone/pull the latest version of Lean 11 | - `dotnet`, to run the generator 12 | - `pyright`, to check the generated stubs using the [Pyright](https://github.com/microsoft/pyright) type checker used in [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) (`npm i -g pyright` or `yarn global add pyright`) 13 | 14 | During development it is also useful to check the generated stubs in PyCharm and in VS Code with the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) extension. Good files to open and see if the stubs provide correct information can be found in [QuantConnect/Lean/Algorithm.Python](https://github.com/QuantConnect/Lean/tree/master/Algorithm.Python). 15 | -------------------------------------------------------------------------------- /integration/pull_repos.py: -------------------------------------------------------------------------------- 1 | # QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 2 | # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | from utils import * 15 | 16 | # Simple setup script that gets Lean and runtime repos into our workspace 17 | # under `generated` directory 18 | def main(): 19 | ensure_command_availability("git") 20 | ensure_command_availability("dotnet") 21 | ensure_command_availability("pyright") 22 | 23 | project_root = Path(__file__).absolute().parent.parent 24 | generated_dir = project_root / "generated" 25 | lean_dir = generated_dir / "Lean" 26 | runtime_dir = generated_dir / "runtime" 27 | stubs_dir = generated_dir / "stubs" 28 | generator_dir = project_root / "QuantConnectStubsGenerator" 29 | 30 | generated_dir.mkdir(parents=True, exist_ok=True) 31 | 32 | ensure_repository_up_to_date("QuantConnect/Lean", lean_dir) 33 | ensure_repository_up_to_date("dotnet/runtime", runtime_dir) 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/ClrStubsRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.IO; 17 | 18 | namespace QuantConnectStubsGenerator.Renderer 19 | { 20 | public class ClrStubsRenderer : BaseRenderer 21 | { 22 | public ClrStubsRenderer(TextWriter writer) : base(writer) 23 | { 24 | } 25 | 26 | public void Render() 27 | { 28 | WriteLine($@" 29 | import typing 30 | 31 | import System 32 | import System.Reflection 33 | 34 | 35 | def getPreload() -> bool: 36 | ... 37 | 38 | 39 | def setPreload(preloadFlag: bool) -> None: 40 | ... 41 | 42 | 43 | def AddReference(name: str) -> System.Reflection.Assembly: 44 | ... 45 | 46 | 47 | def GetClrType(type: typing.Type[typing.Any]) -> System.Type: 48 | ... 49 | 50 | 51 | def FindAssembly(name: str) -> str: 52 | ... 53 | 54 | 55 | def ListAssemblies(verbose: bool) -> typing.List[System.Reflection.Assembly]: 56 | ... 57 | ".Trim()); 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.IO; 18 | using System.Reflection; 19 | using log4net; 20 | using log4net.Config; 21 | 22 | namespace QuantConnectStubsGenerator 23 | { 24 | internal static class Program 25 | { 26 | private static readonly ILog Logger = LogManager.GetLogger(typeof(Program)); 27 | 28 | private static void Main(string[] args) 29 | { 30 | XmlConfigurator.Configure( 31 | LogManager.GetRepository(Assembly.GetEntryAssembly()), 32 | new FileInfo("log4net.config")); 33 | 34 | if (args.Length != 3) 35 | { 36 | Logger.Error("Usage: dotnet run "); 37 | Environment.Exit(1); 38 | } 39 | 40 | try 41 | { 42 | new Generator(args[0], args[1], args[2]).Run(); 43 | } 44 | catch (Exception e) 45 | { 46 | Logger.Error("Generator crashed", e); 47 | Environment.Exit(1); 48 | } 49 | } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/Property.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | namespace QuantConnectStubsGenerator.Model 17 | { 18 | public class Property : CodeEntity 19 | { 20 | public string Name { get; } 21 | 22 | public PythonType Type { get; set; } 23 | 24 | public bool Static { get; set; } 25 | 26 | public bool Abstract { get; set; } 27 | 28 | public bool Constant { get; set; } 29 | 30 | public string Value { get; set; } 31 | 32 | public string DeprecationReason { get; set; } 33 | 34 | public bool HasSetter { get; set; } 35 | 36 | public Class Class { get; set; } 37 | 38 | public Property(string name) 39 | { 40 | Name = name; 41 | } 42 | 43 | public Property(Property template, string name) : this(name) 44 | { 45 | Type = template.Type; 46 | Static = template.Static; 47 | Abstract = template.Abstract; 48 | Constant = template.Constant; 49 | Value = template.Value; 50 | Summary = template.Summary; 51 | DeprecationReason = template.DeprecationReason; 52 | HasSetter = template.HasSetter; 53 | Class = template.Class; 54 | Documentation = template.Documentation; 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /integration/utils.py: -------------------------------------------------------------------------------- 1 | # QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 2 | # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import os 15 | import shutil 16 | import subprocess 17 | import sys 18 | from importlib.util import find_spec 19 | from pathlib import Path 20 | 21 | def fail(msg): 22 | print(msg, file=sys.stderr) 23 | sys.exit(1) 24 | 25 | 26 | def run_command(args, cwd=os.getcwd(), env=None, append_empty_line=True): 27 | try: 28 | print(f"Running {[str(arg) for arg in args] if len(args) <= 10 else args[0]} in {cwd}", flush=True) 29 | proc = subprocess.run(args, cwd=cwd, env=env) 30 | 31 | if append_empty_line: 32 | print(flush=True) 33 | 34 | return proc.returncode == 0 35 | except FileNotFoundError: 36 | return False 37 | 38 | 39 | def ensure_command_availability(command): 40 | if not run_command([command, "--version"]): 41 | fail(f"{command} is not available") 42 | 43 | 44 | def ensure_repository_up_to_date(repo, repo_dir): 45 | if repo_dir.exists(): 46 | if not run_command(["git", "pull"], cwd=repo_dir): 47 | fail(f"Could not pull {repo}") 48 | else: 49 | if not run_command(["git", "clone", "--depth", "1", f"https://github.com/{repo}.git", repo_dir]): 50 | fail(f"Could not clone {repo}") 51 | 52 | 53 | def get_python_files(dir): 54 | for dirpath, _, files in os.walk(dir): 55 | for file in files: 56 | if file.endswith(".pyi"): 57 | yield os.path.abspath(os.path.join(dirpath, file)) -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/Utility/DependencyGraphTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Linq; 17 | using NUnit.Framework; 18 | using QuantConnectStubsGenerator.Model; 19 | using QuantConnectStubsGenerator.Utility; 20 | 21 | namespace QuantConnectStubsGenerator.Tests.Utility 22 | { 23 | [TestFixture] 24 | public class DependencyGraphTests 25 | { 26 | [Test] 27 | public void GetClassesInOrderShouldReturnTheRegisteredClassesInSuchOrderToMinimizeForwardReferences() 28 | { 29 | var classA = new Class(new PythonType("A")); 30 | var classB = new Class(new PythonType("B")); 31 | var classC = new Class(new PythonType("C")); 32 | 33 | var dependencyGraph = new DependencyGraph(); 34 | 35 | dependencyGraph.AddClass(classA); 36 | dependencyGraph.AddClass(classB); 37 | dependencyGraph.AddClass(classC); 38 | 39 | dependencyGraph.AddDependency(classA, classB.Type); 40 | dependencyGraph.AddDependency(classA, classC.Type); 41 | dependencyGraph.AddDependency(classB, classC.Type); 42 | 43 | var classes = dependencyGraph.GetClassesInOrder().ToList(); 44 | 45 | Assert.AreEqual(3, classes.Count); 46 | 47 | Assert.AreEqual(classC, classes[0]); 48 | Assert.AreEqual(classB, classes[1]); 49 | Assert.AreEqual(classA, classes[2]); 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Utility/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | 19 | namespace QuantConnectStubsGenerator.Utility 20 | { 21 | public static class StringExtensions 22 | { 23 | private static readonly HashSet _reservedPythonWord = new HashSet() 24 | { 25 | "and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", 26 | "False", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", 27 | "nonlocal", "not", "or", "pass", "raise", "return", "True", "try", "while", "with", "yield", 28 | "await" 29 | }; 30 | 31 | public static string Indent(this string str, int level = 1) 32 | { 33 | var indentation = new string(' ', level * 4); 34 | 35 | var lines = str 36 | .Split("\n") 37 | .Select(line => indentation + line); 38 | 39 | return string.Join("\n", lines); 40 | } 41 | 42 | public static string ToSnakeCase(this string text, bool constant = false) 43 | { 44 | var result = Python.Runtime.Util.ToSnakeCase(text); 45 | return constant ? result.ToUpperInvariant() : result; 46 | } 47 | 48 | public static bool IsPythonReservedWord(this string word) 49 | { 50 | return _reservedPythonWord.Contains(word); 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/ParseContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | 19 | namespace QuantConnectStubsGenerator.Model 20 | { 21 | /// 22 | /// The ParseContext is the root container which is filled with information gathered from the C# files. 23 | /// 24 | /// At the start of the program one instance of this class is created and passed to all parsers. 25 | /// The parsers are responsible for filling the ParseContext with all relevant information. 26 | /// Afterwards, this information is used by the renderers to create the necessary Python stubs. 27 | /// 28 | public class ParseContext 29 | { 30 | private readonly IDictionary _namespaces = new Dictionary(); 31 | 32 | public IEnumerable GetNamespaces() 33 | { 34 | return _namespaces.Values; 35 | } 36 | 37 | public Namespace GetNamespaceByName(string name) 38 | { 39 | if (_namespaces.ContainsKey(name)) 40 | { 41 | return _namespaces[name]; 42 | } 43 | 44 | throw new ArgumentException($"No namespace has been registered with name '{name}'"); 45 | } 46 | 47 | public bool HasNamespace(string name) 48 | { 49 | return _namespaces.ContainsKey(name); 50 | } 51 | 52 | public void RegisterNamespace(Namespace ns) 53 | { 54 | _namespaces[ns.Name] = ns; 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/Parameter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | 18 | namespace QuantConnectStubsGenerator.Model 19 | { 20 | public class Parameter : IEquatable 21 | { 22 | public string Name { get; } 23 | public PythonType Type { get; set; } 24 | 25 | public bool VarArgs { get; set; } 26 | public string Value { get; set; } 27 | 28 | public Parameter(string name, PythonType type) 29 | { 30 | Name = name; 31 | Type = type; 32 | } 33 | 34 | public Parameter(Parameter other) 35 | : this(other.Name, other.Type) 36 | { 37 | VarArgs = other.VarArgs; 38 | Value = other.Value; 39 | } 40 | 41 | public bool Equals(Parameter other) 42 | { 43 | if (ReferenceEquals(null, other)) return false; 44 | if (ReferenceEquals(this, other)) return true; 45 | return Name == other.Name 46 | && Type.Equals(other.Type) 47 | && VarArgs == other.VarArgs 48 | && Value == other.Value; 49 | } 50 | 51 | public override bool Equals(object obj) 52 | { 53 | if (ReferenceEquals(null, obj)) return false; 54 | if (ReferenceEquals(this, obj)) return true; 55 | return obj.GetType() == GetType() && Equals((Parameter)obj); 56 | } 57 | 58 | public override int GetHashCode() 59 | { 60 | return HashCode.Combine(Name, Type, VarArgs, Value); 61 | } 62 | 63 | public override string ToString() 64 | { 65 | return $"{Name}: {Type}"; 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/Namespace.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Linq; 19 | 20 | namespace QuantConnectStubsGenerator.Model 21 | { 22 | public class Namespace 23 | { 24 | public string Name { get; } 25 | 26 | public List NamespacesToImport { get; set; } 27 | 28 | private readonly IDictionary _classes = new Dictionary(); 29 | 30 | public Namespace(string name) 31 | { 32 | Name = name; 33 | } 34 | 35 | public IEnumerable GetClasses() 36 | { 37 | return _classes.Values; 38 | } 39 | 40 | public IEnumerable GetParentClasses() 41 | { 42 | return _classes.Values.Where(cls => cls.ParentClass == null); 43 | } 44 | 45 | public Class GetClassByType(PythonType type) 46 | { 47 | var key = GetClassKey(type); 48 | 49 | if (_classes.ContainsKey(key)) 50 | { 51 | return _classes[key]; 52 | } 53 | 54 | throw new ArgumentException($"No class has been registered with type '{type.ToPythonString()}'"); 55 | } 56 | 57 | public bool HasClass(PythonType type) 58 | { 59 | return _classes.ContainsKey(GetClassKey(type)); 60 | } 61 | 62 | public void RegisterClass(Class cls) 63 | { 64 | _classes[GetClassKey(cls)] = cls; 65 | } 66 | 67 | private string GetClassKey(PythonType type) 68 | { 69 | return $"{type.Namespace}.{type.Name}"; 70 | } 71 | 72 | private string GetClassKey(Class cls) 73 | { 74 | return GetClassKey(cls.Type); 75 | } 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/PyLoaderRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.IO; 17 | 18 | namespace QuantConnectStubsGenerator.Renderer 19 | { 20 | public class PyLoaderRenderer : BaseRenderer 21 | { 22 | public PyLoaderRenderer(TextWriter writer) : base(writer) 23 | { 24 | } 25 | 26 | public void Render(string ns) 27 | { 28 | var baseNamespace = ns.Split(".")[0]; 29 | var assembly = ns == "QuantConnect" ? "QuantConnect.Common" : ns; 30 | 31 | WriteLine($@" 32 | import os 33 | import sys 34 | 35 | # If quantconnect-stubs is installed via pip and Lean is ran locally, 36 | # importing anything from the current namespace makes the Python 37 | # interpreter look in the quantconnect-stubs package for the implementation. 38 | # 39 | # The desired behavior is for the interpreter to use the implementation 40 | # provided by the AddReference() call from Python.NET. 41 | # 42 | # To fix this, we temporarily remove the directory containing the 43 | # quantconnect-stubs package from sys.path and re-import the current namespace 44 | # so the relevant C# namespace is used when running Lean locally. 45 | 46 | # Find the directory containing quantconnect-stubs (usually site-packages) 47 | current_path = os.path.dirname(__file__) 48 | while os.path.basename(current_path) != ""{baseNamespace}"": 49 | current_path = os.path.dirname(current_path) 50 | current_path = os.path.dirname(current_path) 51 | 52 | # Temporarily remove the directory containing quantconnect-stubs from sys.path 53 | original_path = sys.path[:] 54 | sys.path.remove(current_path) 55 | 56 | # Import the C# version of the current namespace 57 | del sys.modules[""{ns}""] 58 | from clr import AddReference 59 | AddReference(""{assembly}"") 60 | from {ns} import * 61 | 62 | # Restore sys.path 63 | sys.path = original_path 64 | ".Trim()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/Model/ParseContextTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Linq; 18 | using NUnit.Framework; 19 | using QuantConnectStubsGenerator.Model; 20 | 21 | namespace QuantConnectStubsGenerator.Tests.Model 22 | { 23 | [TestFixture] 24 | public class ParseContextTests 25 | { 26 | [Test] 27 | public void GetNamespacesShouldReturnAllRegisteredNamespaces() 28 | { 29 | var context = new ParseContext(); 30 | 31 | var ns1 = new Namespace("ns1"); 32 | var ns2 = new Namespace("ns2"); 33 | 34 | context.RegisterNamespace(ns1); 35 | context.RegisterNamespace(ns2); 36 | 37 | var namespaces = context.GetNamespaces().ToList(); 38 | 39 | Assert.AreEqual(2, namespaces.Count); 40 | Assert.IsTrue(namespaces.Contains(ns1)); 41 | Assert.IsTrue(namespaces.Contains(ns2)); 42 | } 43 | 44 | [Test] 45 | public void GetNamespaceByNameShouldReturnNamespaceIfItHasBeenRegistered() 46 | { 47 | var context = new ParseContext(); 48 | var ns = new Namespace("Test"); 49 | 50 | context.RegisterNamespace(ns); 51 | 52 | Assert.AreEqual(ns, context.GetNamespaceByName(ns.Name)); 53 | } 54 | 55 | [Test] 56 | public void GetNamespaceByNameShouldThrowIfNamespaceHasNotBeenRegistered() 57 | { 58 | var context = new ParseContext(); 59 | 60 | Assert.Throws(() => context.GetNamespaceByName("Test")); 61 | } 62 | 63 | [Test] 64 | public void HasNamespaceShouldReturnTrueIfNamespaceHasBeenRegistered() 65 | { 66 | var context = new ParseContext(); 67 | var ns = new Namespace("Test"); 68 | context.RegisterNamespace(ns); 69 | 70 | Assert.IsTrue(context.HasNamespace(ns.Name)); 71 | } 72 | 73 | [Test] 74 | public void HasNamespaceShouldReturnFalseIfNamespaceHasNotBeenRegistered() 75 | { 76 | var context = new ParseContext(); 77 | 78 | Assert.IsFalse(context.HasNamespace("Test")); 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuantConnect Stubs Generator 2 | 3 | [![Build Status](https://github.com/QuantConnect/quantconnect-stubs-generator/workflows/Build/badge.svg)](https://github.com/QuantConnect/quantconnect-stubs-generator/actions?query=workflow%3ABuild) 4 | [![PyPI Version](https://img.shields.io/pypi/v/quantconnect-stubs)](https://pypi.org/project/quantconnect-stubs/) 5 | [![PyPI Downloads](https://img.shields.io/pypi/dm/quantconnect-stubs)](https://pypi.org/project/quantconnect-stubs/) 6 | 7 | QuantConnect Stubs Generator is a program which generates Python stubs based on the C# files in [QuantConnect/Lean](https://github.com/QuantConnect/Lean) and [dotnet/runtime](https://github.com/dotnet/runtime). These stubs can be used by editors to provide type-aware features like autocomplete and auto-imports in QuantConnect strategies written in Python. 8 | 9 | ## Installation 10 | 11 | The latest version of the stubs can be installed by running `pip install --upgrade quantconnect-stubs`. Every time Lean is updated, a new version of the package is released containing the latest stubs (the same command can be used to update). 12 | 13 | The stubs are tested to work well with PyCharm and VS Code in combination with the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) extension. They should also work with any other editor capable of indexing Python type stubs. 14 | 15 | If type-aware features like autocomplete are not working after installing the package, make sure your editor supports indexing Python type stubs and is set up to index packages in the environment you installed the package into. Sometimes it may also help to restart your editor to make sure newly installed/updated packages are correctly indexed. 16 | 17 | After installing the stubs, you can copy the following line to the top of every Python file to have the same imports as the ones that are added by default in the cloud: 18 | ```py 19 | from AlgorithmImports import * 20 | ``` 21 | 22 | This line imports [all common QuantConnect members](https://github.com/QuantConnect/Lean/blob/master/Common/AlgorithmImports.py) and provides autocomplete for them. 23 | 24 | ## Development 25 | 26 | To run the generator locally, clone the repository, `cd` into the QuantConnectStubsGenerator project and run `dotnet run `. Make sure `` points to a directory containing the [QuantConnect/Lean](https://github.com/QuantConnect/Lean) repository and `` points to a directory containing the [dotnet/runtime](https://github.com/dotnet/runtime) repository. 27 | 28 | To run the unit tests, run `dotnet test` in the root of the project. To run the integration tests read the [`integration/README.md`](./integration/README.md) file. 29 | 30 | 31 | ### Using Dev Container 32 | 33 | - Open VSCode, click "Reopen in Container Prompt"; Or use command pallete "Reopen in Container" 34 | - After build and initial creation script completes, run VSCode launch option "Run Stubs Generator" 35 | 36 | There is also a launch option for integration tests which works right out of the box in this dev container. Try option "Python Integration Tests" 37 | 38 | 39 | -------------------------------------------------------------------------------- /integration/integration_tests.py: -------------------------------------------------------------------------------- 1 | # QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 2 | # Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | import sys 15 | from utils import * 16 | 17 | def main(): 18 | if find_spec("QuantConnect") is not None: 19 | fail("Integration tests must run in an environment in which the stubs are not already installed") 20 | 21 | for package in ["pandas", "matplotlib"]: 22 | if find_spec(package) is None: 23 | fail(f"{package} must be installed when running the integration tests") 24 | 25 | ensure_command_availability("git") 26 | ensure_command_availability("dotnet") 27 | ensure_command_availability("pyright") 28 | ensure_command_availability("mypy") 29 | 30 | project_root = Path(__file__).absolute().parent.parent 31 | generated_dir = project_root / "generated" 32 | lean_dir = generated_dir / "Lean" 33 | runtime_dir = generated_dir / "runtime" 34 | stubs_dir = generated_dir / "stubs" 35 | generator_dir = project_root / "QuantConnectStubsGenerator" 36 | 37 | generated_dir.mkdir(parents=True, exist_ok=True) 38 | 39 | ensure_repository_up_to_date("QuantConnect/Lean", lean_dir) 40 | ensure_repository_up_to_date("dotnet/runtime", runtime_dir) 41 | 42 | if stubs_dir.exists(): 43 | shutil.rmtree(stubs_dir) 44 | 45 | stubs_version = "1.0.0" 46 | env = os.environ.copy() 47 | env["STUBS_VERSION"] = stubs_version 48 | if not run_command(["dotnet", "run", lean_dir, runtime_dir, stubs_dir], cwd=generator_dir, env=env): 49 | fail("Could not run QuantConnectStubsGenerator") 50 | 51 | with open(stubs_dir / "pyrightconfig.json", "w") as file: 52 | file.write(f""" 53 | {{ 54 | "include": [{", ".join([f'"{ns}/**"' for ns in os.listdir(stubs_dir)])}], 55 | "exclude": ["System/Collections/Immutable/**", "System/__init__.pyi", "System/Runtime/InteropServices/**"], 56 | "reportGeneralTypeIssues": "none", 57 | "reportInvalidTypeVarUse": "none", 58 | "reportWildcardImportFromLibrary": "none" 59 | }} 60 | """.strip()) 61 | 62 | if not run_command(["pyright"], cwd=stubs_dir): 63 | fail("Pyright found errors in the generated stubs") 64 | 65 | if (not run_command([sys.executable, "setup.py", "--quiet", "sdist", "bdist_wheel"], cwd=stubs_dir) or 66 | not run_command([sys.executable, "-m", "pip", "install", "--force-reinstall", f"dist/quantconnect_stubs-{stubs_version}-py3-none-any.whl"], cwd=stubs_dir)): 67 | fail("Could not build and install the stubs") 68 | 69 | run_command([sys.executable, "run_syntax_check.py"], cwd=lean_dir, append_empty_line=False) 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Utility/Extensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using QuantConnectStubsGenerator.Model; 17 | using System.Collections.Generic; 18 | using System; 19 | 20 | namespace QuantConnectStubsGenerator.Utility 21 | { 22 | public static class Extensions 23 | { 24 | private static bool TryGetClass(PythonType classType, ParseContext context, out Class @class) 25 | { 26 | @class = null; 27 | if (classType == null) 28 | { 29 | return false; 30 | } 31 | 32 | try 33 | { 34 | var @namespace = context.GetNamespaceByName(classType.Namespace); 35 | @class = @namespace.GetClassByType(classType); 36 | return true; 37 | } 38 | catch (ArgumentException) 39 | { 40 | // Class not found: 41 | // - The type was converted to a Python type (e.g. DateTime -> datetime), 42 | // so the class will not be found in any namespace, no need to check. Or, 43 | // - The class is private or internal, it won't be found in the namespaces. 44 | return false; 45 | } 46 | } 47 | 48 | public static IEnumerable GetBaseClasses(this Class cls, ParseContext context) 49 | { 50 | var baseClassTypes = new Queue(cls.InheritsFrom); 51 | var checkedClassTypes = new HashSet(); 52 | 53 | while (baseClassTypes.TryDequeue(out var baseClassType)) 54 | { 55 | if (TryGetClass(baseClassType, context, out var baseClass)) 56 | { 57 | yield return baseClass; 58 | 59 | checkedClassTypes.Add(baseClassType); 60 | 61 | foreach (var baseBaseClass in baseClass.InheritsFrom) 62 | { 63 | if (!checkedClassTypes.Contains(baseBaseClass)) 64 | { 65 | baseClassTypes.Enqueue(baseBaseClass); 66 | checkedClassTypes.Add(baseBaseClass); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | public static IEnumerable GetClassAndBaseClasses(this Class cls, ParseContext context) 74 | { 75 | yield return cls; 76 | foreach (var baseClass in cls.GetBaseClasses(context)) 77 | { 78 | yield return baseClass; 79 | } 80 | } 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/ObjectRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.IO; 18 | using System.Linq; 19 | using QuantConnectStubsGenerator.Model; 20 | using QuantConnectStubsGenerator.Utility; 21 | 22 | namespace QuantConnectStubsGenerator.Renderer 23 | { 24 | public abstract class ObjectRenderer : BaseRenderer 25 | { 26 | private readonly int _indentationLevel; 27 | 28 | private readonly string _indentation; 29 | private bool _isAtLineStart = true; 30 | 31 | protected ParseContext Context { get; } 32 | 33 | protected ObjectRenderer(TextWriter writer, int indentationLevel, ParseContext context) : base(writer) 34 | { 35 | _indentationLevel = indentationLevel; 36 | 37 | _indentation = new string(' ', indentationLevel * 4); 38 | Context = context; 39 | } 40 | 41 | public abstract void Render(T item); 42 | 43 | protected override void Write(string value) 44 | { 45 | base.Write(IndentIfNecessary(value)); 46 | _isAtLineStart = value.EndsWith("\n"); 47 | } 48 | 49 | protected override void WriteLine(string value = "") 50 | { 51 | base.WriteLine(IndentIfNecessary(value).TrimEnd()); 52 | _isAtLineStart = true; 53 | } 54 | 55 | protected void WriteSummary(string summary, bool indented = false) 56 | { 57 | if (summary == null) 58 | { 59 | return; 60 | } 61 | 62 | if (!summary.Contains("\n") && summary.EndsWith("\"")) 63 | { 64 | summary = summary.Substring(0, summary.Length - 1) + "\\\""; 65 | } 66 | 67 | var newline = summary.Contains("\n") ? "\n" : ""; 68 | var docstring = $"\"\"\"{newline}{summary}{newline}\"\"\""; 69 | 70 | WriteLine(docstring.Indent(indented ? 1 : 0)); 71 | } 72 | 73 | protected TRenderer CreateRenderer(bool indented = true) 74 | { 75 | var indentationLevel = _indentationLevel + (indented ? 1 : 0); 76 | return (TRenderer) Activator.CreateInstance(typeof(TRenderer), Writer, indentationLevel, Context); 77 | } 78 | 79 | private string IndentIfNecessary(string value) 80 | { 81 | if (!_isAtLineStart) 82 | { 83 | return value; 84 | } 85 | 86 | var lines = value 87 | .Split("\n") 88 | .Select(line => _indentation + line); 89 | 90 | return string.Join("\n", lines); 91 | } 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Utility/DependencyGraph.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Linq; 19 | using QuantConnectStubsGenerator.Model; 20 | using QuikGraph; 21 | using QuikGraph.Algorithms; 22 | 23 | namespace QuantConnectStubsGenerator.Utility 24 | { 25 | /// 26 | /// The DependencyGraph is used by the NamespaceRenderer as a sort of queue 27 | /// to render classes in such order to limit the amount of forward references. 28 | /// 29 | public class DependencyGraph 30 | { 31 | private readonly IDictionary _classes = new Dictionary(); 32 | 33 | private readonly AdjacencyGraph> _graph = 34 | new AdjacencyGraph>(); 35 | 36 | public void AddClass(Class cls) 37 | { 38 | _classes[cls.Type] = cls; 39 | _graph.AddVertex(cls.Type); 40 | } 41 | 42 | public void AddDependency(Class cls, PythonType type) 43 | { 44 | if (!_classes.ContainsKey(cls.Type)) 45 | { 46 | throw new ArgumentException($"'{cls.Type.ToPythonString()}' has not been registered using AddClass"); 47 | } 48 | 49 | type = GetParentType(type); 50 | 51 | // Classes can't depend on themselves 52 | if (Equals(cls.Type, type)) 53 | { 54 | return; 55 | } 56 | 57 | // Only dependencies between the registered classes are considered 58 | if (!_classes.ContainsKey(type)) 59 | { 60 | return; 61 | } 62 | 63 | var edge = new Edge(cls.Type, type); 64 | _graph.AddEdge(edge); 65 | 66 | // We can't determine the best class order if there are cycles in their dependencies 67 | // If the new dependency creates a cycle, remove it 68 | if (!_graph.IsDirectedAcyclicGraph()) 69 | { 70 | _graph.RemoveEdge(edge); 71 | } 72 | } 73 | 74 | public IEnumerable GetClassesInOrder() 75 | { 76 | return _graph.TopologicalSort().Select(type => _classes[type]).Reverse(); 77 | } 78 | 79 | private PythonType GetParentType(PythonType type) 80 | { 81 | if (!type.Name.Contains(".")) 82 | { 83 | return type; 84 | } 85 | 86 | return new PythonType(type.Name.Substring(0, type.Name.IndexOf('.')), type.Namespace) 87 | { 88 | Alias = type.Alias, 89 | IsNamedTypeParameter = type.IsNamedTypeParameter, 90 | TypeParameters = type.TypeParameters 91 | }; 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/Model/PythonTypeTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using NUnit.Framework; 17 | using QuantConnectStubsGenerator.Model; 18 | 19 | namespace QuantConnectStubsGenerator.Tests.Model 20 | { 21 | [TestFixture] 22 | public class PythonTypeTests 23 | { 24 | [Test] 25 | public void ToPythonStringCorrectlyFormatsAlias() 26 | { 27 | var type = new PythonType("Any", "typing") 28 | { 29 | Alias = "AnyAlias" 30 | }; 31 | 32 | Assert.AreEqual("AnyAlias", type.ToPythonString()); 33 | } 34 | 35 | [Test] 36 | public void ToPythonStringCorrectlyIgnoresAlias() 37 | { 38 | var type = new PythonType("Any", "typing") 39 | { 40 | Alias = "AnyAlias" 41 | }; 42 | 43 | Assert.AreEqual("typing.Any", type.ToPythonString(true)); 44 | } 45 | 46 | [Test] 47 | public void ToPythonStringCorrectlyFormatsNamedTypeParameter() 48 | { 49 | var type = new PythonType("MyClass.TKey", "QuantConnect.Data") 50 | { 51 | IsNamedTypeParameter = true 52 | }; 53 | 54 | Assert.AreEqual("QuantConnect_Data_MyClass_TKey", type.ToPythonString()); 55 | } 56 | 57 | [Test] 58 | public void ToPythonStringCorrectlyAddsNamespace() 59 | { 60 | var type = new PythonType("MyClass", "QuantConnect"); 61 | 62 | Assert.AreEqual("QuantConnect.MyClass", type.ToPythonString()); 63 | } 64 | 65 | [Test] 66 | public void ToPythonStringOmitsNamespaceWhenNamespaceIsNull() 67 | { 68 | var type = new PythonType("MyClass"); 69 | 70 | Assert.AreEqual("MyClass", type.ToPythonString()); 71 | } 72 | 73 | [Test] 74 | public void ToPythonStringCorrectlyFormatsTypeParameters() 75 | { 76 | var type = new PythonType("MyClass", "QuantConnect"); 77 | type.TypeParameters.Add(new PythonType("MyOtherClass", "QuantConnect")); 78 | type.TypeParameters.Add(new PythonType("MyOtherClass2", "QuantConnect")); 79 | 80 | Assert.AreEqual( 81 | "QuantConnect.MyClass[QuantConnect.MyOtherClass, QuantConnect.MyOtherClass2]", 82 | type.ToPythonString()); 83 | } 84 | 85 | [Test] 86 | public void ToPythonStringCorrectlyFormatsCallable() 87 | { 88 | var type = new PythonType("Callable", "typing"); 89 | type.TypeParameters.Add(new PythonType("str")); 90 | type.TypeParameters.Add(new PythonType("str")); 91 | type.TypeParameters.Add(new PythonType("str")); 92 | 93 | Assert.AreEqual("typing.Callable[[str, str], str]", type.ToPythonString()); 94 | } 95 | } 96 | } 97 | 98 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/Model/NamespaceTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Linq; 18 | using NUnit.Framework; 19 | using QuantConnectStubsGenerator.Model; 20 | 21 | namespace QuantConnectStubsGenerator.Tests.Model 22 | { 23 | [TestFixture] 24 | public class NamespaceTests 25 | { 26 | [Test] 27 | public void GetClassesShouldReturnAllRegisteredClasses() 28 | { 29 | var ns = new Namespace("Test"); 30 | 31 | var parentCls = new Class(new PythonType("ParentClass", "QuantConnect")); 32 | var childCls = new Class(new PythonType("ChildClass", "QuantConnect")) 33 | { 34 | ParentClass = parentCls 35 | }; 36 | 37 | parentCls.InnerClasses.Add(childCls); 38 | 39 | ns.RegisterClass(parentCls); 40 | ns.RegisterClass(childCls); 41 | 42 | var classes = ns.GetClasses().ToList(); 43 | 44 | Assert.AreEqual(2, classes.Count); 45 | Assert.AreEqual(parentCls, classes[0]); 46 | Assert.AreEqual(childCls, classes[1]); 47 | } 48 | 49 | [Test] 50 | public void GetParentClassesShouldReturnAllRegisteredParentClasses() 51 | { 52 | var ns = new Namespace("Test"); 53 | 54 | var parentCls = new Class(new PythonType("ParentClass", "QuantConnect")); 55 | var childCls = new Class(new PythonType("ChildClass", "QuantConnect")) 56 | { 57 | ParentClass = parentCls 58 | }; 59 | 60 | parentCls.InnerClasses.Add(childCls); 61 | 62 | ns.RegisterClass(parentCls); 63 | ns.RegisterClass(childCls); 64 | 65 | var parentClasses = ns.GetParentClasses().ToList(); 66 | 67 | Assert.AreEqual(1, parentClasses.Count); 68 | Assert.AreEqual(parentCls, parentClasses[0]); 69 | } 70 | 71 | [Test] 72 | public void GetClassByTypeShouldReturnThePreviouslyRegisteredClass() 73 | { 74 | var ns = new Namespace("Test"); 75 | var cls = new Class(new PythonType("MyClass", "QuantConnect")); 76 | ns.RegisterClass(cls); 77 | 78 | Assert.AreEqual(cls, ns.GetClassByType(cls.Type)); 79 | } 80 | 81 | [Test] 82 | public void GetClassByTypeShouldThrowIfNotRegistered() 83 | { 84 | var ns = new Namespace("Test"); 85 | 86 | Assert.Throws(() => ns.GetClassByType(new PythonType("MyClass", "QuantConnect"))); 87 | } 88 | 89 | [Test] 90 | public void HasClassShouldReturnTrueIfClassHasBeenRegistered() 91 | { 92 | var ns = new Namespace("Test"); 93 | var cls = new Class(new PythonType("MyClass", "QuantConnect")); 94 | ns.RegisterClass(cls); 95 | 96 | Assert.IsTrue(ns.HasClass(cls.Type)); 97 | } 98 | 99 | [Test] 100 | public void HasClassShouldReturnFalseIfClassHasNotBeenRegistered() 101 | { 102 | var ns = new Namespace("Test"); 103 | 104 | Assert.IsFalse(ns.HasClass(new PythonType("MyClass", "QuantConnect"))); 105 | } 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/Method.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Linq; 19 | 20 | namespace QuantConnectStubsGenerator.Model 21 | { 22 | public class Method : CodeEntity, IEquatable 23 | { 24 | public string Name { get; } 25 | public PythonType ReturnType { get; set; } 26 | 27 | public bool Static { get; set; } 28 | public bool Overload { get; set; } 29 | 30 | public string File { get; set; } 31 | 32 | public bool IsGeneric => GenericType != null; 33 | 34 | public PythonType GenericType { get; set; } 35 | 36 | public string DeprecationReason { get; set; } 37 | 38 | public IList Parameters { get; } 39 | 40 | public Class Class { get; set; } 41 | 42 | public bool AvoidImplicitTypes { get; set; } 43 | 44 | public Method(string name, PythonType returnType) 45 | { 46 | Name = name; 47 | ReturnType = returnType; 48 | Parameters = new List(); 49 | } 50 | public Method(string name, Method other) : this(name, other.ReturnType) 51 | { 52 | File = other.File; 53 | Static = other.Static; 54 | Summary = other.Summary; 55 | Overload = other.Overload; 56 | // Create a new list, so it can be modified for each method separately 57 | Parameters = other.Parameters.ToList(); 58 | GenericType = other.GenericType; 59 | DeprecationReason = other.DeprecationReason; 60 | Documentation = other.Documentation; 61 | } 62 | 63 | public bool Equals(Method other) 64 | { 65 | if (ReferenceEquals(null, other)) return false; 66 | if (ReferenceEquals(this, other)) return true; 67 | 68 | if (Name != other.Name 69 | || !Class.Equals(other.Class) 70 | || Static != other.Static 71 | || Overload != other.Overload 72 | || GenericType != other.GenericType 73 | || Parameters.Count != other.Parameters.Count) 74 | { 75 | return false; 76 | } 77 | 78 | // Don't use Parameter.Equals() because a different parameter name does not make a method different 79 | for (var i = 0; i < Parameters.Count; i++) 80 | { 81 | var parameter = Parameters[i]; 82 | var otherParameter = other.Parameters[i]; 83 | if (!parameter.Type.Equals(otherParameter.Type) 84 | || parameter.VarArgs != otherParameter.VarArgs 85 | || parameter.Value != otherParameter.Value) 86 | { 87 | return false; 88 | } 89 | } 90 | 91 | return true; 92 | } 93 | 94 | public override bool Equals(object obj) 95 | { 96 | if (ReferenceEquals(null, obj)) return false; 97 | if (ReferenceEquals(this, obj)) return true; 98 | return obj.GetType() == GetType() && Equals((Method)obj); 99 | } 100 | 101 | public override int GetHashCode() 102 | { 103 | var hash = 19; 104 | foreach (var parameter in Parameters) 105 | { 106 | hash = hash * 31 + parameter.GetHashCode(); 107 | } 108 | 109 | return HashCode.Combine(Name, Static, Overload, Class, GenericType, hash); 110 | } 111 | 112 | public override string ToString() 113 | { 114 | var genericRepr = IsGeneric ? $"[{GenericType}]" : string.Empty; 115 | return $"{Name}{genericRepr}({string.Join(", ", Parameters)}) -> {ReturnType}"; 116 | } 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/ClassRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.IO; 18 | using System.Linq; 19 | using System.Collections.Generic; 20 | using QuantConnectStubsGenerator.Model; 21 | 22 | namespace QuantConnectStubsGenerator.Renderer 23 | { 24 | public class ClassRenderer : ObjectRenderer 25 | { 26 | public ClassRenderer(TextWriter writer, int indentationLevel, ParseContext context) 27 | : base(writer, indentationLevel, context) 28 | { 29 | } 30 | 31 | public override void Render(Class cls) 32 | { 33 | RenderClassHeader(cls); 34 | RenderInnerClasses(cls); 35 | RenderProperties(cls); 36 | RenderMethods(cls); 37 | } 38 | 39 | private void RenderClassHeader(Class cls) 40 | { 41 | Write($"class {cls.Type.Name.Split(".").Last()}"); 42 | 43 | var inherited = new List(); 44 | 45 | if (cls.Type.TypeParameters.Count > 0) 46 | { 47 | var types = cls.Type.TypeParameters.Select(type => type.ToPythonString()); 48 | inherited.Add($"typing.Generic[{string.Join(", ", types)}]"); 49 | } 50 | 51 | foreach (var inheritedType in cls.InheritsFrom) 52 | { 53 | inherited.Add(inheritedType.ToPythonString()); 54 | } 55 | 56 | if (cls.MetaClass != null) 57 | { 58 | inherited.Add($"metaclass={cls.MetaClass.ToPythonString()}"); 59 | } 60 | 61 | if (inherited.Count > 0) 62 | { 63 | for (var i = 0; i < inherited.Count; i++) 64 | { 65 | if (inherited[i].Equals("System.Enum", StringComparison.InvariantCultureIgnoreCase)) 66 | { 67 | // 'IntEnum' is a python base type which is handled better by mypy if we used 'System' it assumes the enum value and causes a warning/missmatch. 68 | // We use IntEnum to hint that the enum values are integers which is the case for all QuantConnect enums. 69 | inherited[i] = "IntEnum"; 70 | } 71 | } 72 | Write($"({string.Join(", ", inherited)})"); 73 | } 74 | 75 | WriteLine(":"); 76 | 77 | WriteSummary(cls.Summary ?? "This class has no documentation.", true); 78 | WriteLine(); 79 | } 80 | 81 | private void RenderInnerClasses(Class cls) 82 | { 83 | var classRenderer = CreateRenderer(); 84 | 85 | foreach (var innerClass in cls.InnerClasses) 86 | { 87 | classRenderer.Render(innerClass); 88 | } 89 | } 90 | 91 | private void RenderProperties(Class cls) 92 | { 93 | var propertyRenderer = CreateRenderer(); 94 | 95 | foreach (var property in cls.Properties) 96 | { 97 | propertyRenderer.Render(property); 98 | } 99 | } 100 | 101 | private void RenderMethods(Class cls) 102 | { 103 | var methodRenderer = CreateRenderer(); 104 | 105 | // Some methods have two variants where one is deprecated 106 | // PyCharm complains if you override the second/third/fourth/etc. overload of a method 107 | // We therefore need to render deprecated methods after non-deprecated ones 108 | // This way PyCharm doesn't complain if you override the non-deprecated method 109 | var orderedMethods = cls.Methods 110 | .Where(m => !string.IsNullOrEmpty(m.Name)) 111 | .OrderBy(m => m.Name) 112 | .ThenBy(m => m.DeprecationReason == null ? 0 : 1) 113 | // pyobjects are converted into typing.any, we want those at the top so they resolve first which is what actually happens 114 | .ThenBy(m => m.Parameters.Any(x => x.Type.Name.Equals("Any")) ? 0 : 1); 115 | 116 | foreach (var method in orderedMethods) 117 | { 118 | methodRenderer.Render(method); 119 | } 120 | } 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/NamespaceRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.IO; 19 | using System.Linq; 20 | using QuantConnectStubsGenerator.Model; 21 | using QuantConnectStubsGenerator.Utility; 22 | 23 | namespace QuantConnectStubsGenerator.Renderer 24 | { 25 | public class NamespaceRenderer : ObjectRenderer 26 | { 27 | public NamespaceRenderer(TextWriter writer, int indentationLevel, ParseContext context) 28 | : base(writer, indentationLevel, context) 29 | { 30 | } 31 | 32 | public override void Render(Namespace ns) 33 | { 34 | // Fix for Jedi; Include import of typing instead of using typing.overload 35 | WriteLine("from typing import overload"); 36 | // fix for python enums 37 | WriteLine("from enum import IntEnum"); 38 | 39 | var usedTypes = ns 40 | .GetParentClasses() 41 | .SelectMany(cls => cls.GetUsedTypes()) 42 | .ToList(); 43 | 44 | RenderImports(ns); 45 | RenderTypeAliases(usedTypes); 46 | RenderTypeVars(usedTypes); 47 | 48 | WriteLine(); 49 | 50 | RenderClasses(ns); 51 | } 52 | 53 | private void RenderImports(Namespace ns) 54 | { 55 | var namespacesToImport = ns.NamespacesToImport; 56 | 57 | if (namespacesToImport.Count == 0) 58 | { 59 | return; 60 | } 61 | 62 | var systemNamespaces = namespacesToImport.Where(ns => char.IsLower(ns[0]) && ns != "pandas").ToList(); 63 | var nonSystemNamespaces = namespacesToImport.Except(systemNamespaces).ToList(); 64 | 65 | foreach (var systemNs in systemNamespaces) 66 | { 67 | WriteLine($"import {systemNs}"); 68 | } 69 | 70 | if (systemNamespaces.Count > 0 && nonSystemNamespaces.Count > 0) 71 | { 72 | WriteLine(); 73 | } 74 | 75 | foreach (var nonSystemNs in nonSystemNamespaces) 76 | { 77 | WriteLine($"import {nonSystemNs}"); 78 | } 79 | 80 | WriteLine(); 81 | } 82 | 83 | private void RenderTypeAliases(IEnumerable usedTypes) 84 | { 85 | var typeAliases = usedTypes 86 | .Where(type => type.Alias != null) 87 | .GroupBy(type => type.Alias) 88 | .Select(group => group.First()) 89 | .ToList(); 90 | 91 | if (typeAliases.Count == 0) 92 | { 93 | return; 94 | } 95 | 96 | foreach (var type in typeAliases) 97 | { 98 | WriteLine($"{type.Alias} = {type.ToPythonString(true)}"); 99 | } 100 | 101 | WriteLine(); 102 | } 103 | 104 | private void RenderTypeVars(IEnumerable usedTypes) 105 | { 106 | var typeVars = usedTypes 107 | .Where(type => type.IsNamedTypeParameter) 108 | .Select(type => type.ToPythonString()) 109 | .Distinct() 110 | .ToList(); 111 | 112 | if (typeVars.Count == 0) 113 | { 114 | return; 115 | } 116 | 117 | foreach (var name in typeVars) 118 | { 119 | WriteLine($"{name} = typing.TypeVar(\"{name}\")"); 120 | } 121 | 122 | WriteLine(); 123 | } 124 | 125 | private void RenderClasses(Namespace ns) 126 | { 127 | var dependencyGraph = new DependencyGraph(); 128 | 129 | foreach (var cls in ns.GetParentClasses()) 130 | { 131 | dependencyGraph.AddClass(cls); 132 | } 133 | 134 | foreach (var cls in ns.GetParentClasses()) 135 | { 136 | foreach (var type in cls.GetUsedTypes()) 137 | { 138 | dependencyGraph.AddDependency(cls, type); 139 | } 140 | } 141 | 142 | var classRenderer = CreateRenderer(false); 143 | 144 | foreach (var cls in dependencyGraph.GetClassesInOrder()) 145 | { 146 | classRenderer.Render(cls); 147 | WriteLine(); 148 | } 149 | } 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/Class.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | 19 | namespace QuantConnectStubsGenerator.Model 20 | { 21 | public class Class : CodeEntity 22 | { 23 | public PythonType Type { get; } 24 | 25 | public bool Static { get; set; } 26 | public bool Interface { get; set; } 27 | 28 | public IList InheritsFrom { get; set; } = new List(); 29 | public PythonType MetaClass { get; set; } 30 | 31 | public Class ParentClass { get; set; } 32 | public IList InnerClasses { get; } = new List(); 33 | 34 | public IList Properties { get; } = new List(); 35 | public HashSet Methods { get; } = new HashSet(); 36 | public bool AvoidImplicitTypes { get; set; } 37 | 38 | public Class(PythonType type) 39 | { 40 | Type = type; 41 | } 42 | 43 | public IEnumerable GetUsedTypes() 44 | { 45 | var types = new HashSet(); 46 | 47 | // Parse types recursively to properly return deep generics 48 | var typesToProcess = new Queue(GetUsedTypesToIterateOver()); 49 | 50 | while (typesToProcess.Count > 0) 51 | { 52 | var currentType = typesToProcess.Dequeue(); 53 | 54 | types.Add(currentType); 55 | 56 | foreach (var typeParameter in currentType.TypeParameters) 57 | { 58 | typesToProcess.Enqueue(typeParameter); 59 | } 60 | } 61 | 62 | // Python classes with type parameters always extend typing.Generic[T, ...] where T = typing.TypeVar('T') 63 | if (Type.TypeParameters.Count > 0) 64 | { 65 | types.Add(new PythonType("Generic", "typing")); 66 | types.Add(new PythonType("TypeVar", "typing")); 67 | } 68 | 69 | // PropertyRenderer adds the @abc.abstractmethod decorator to abstract properties 70 | if (Properties.Any(p => !p.Static && p.Abstract)) 71 | { 72 | types.Add(new PythonType("abstractmethod", "abc")); 73 | } 74 | 75 | // PropertyRenderer adds warnings.warn() to deprecated non-static properties 76 | if (Properties.Any(p => p.DeprecationReason != null && !p.Static)) 77 | { 78 | types.Add(new PythonType("warn", "warnings")); 79 | } 80 | 81 | // MethodRenderer adds the @typing.overload decorator to overloaded methods 82 | if (Methods.Any(m => m.Overload)) 83 | { 84 | types.Add(new PythonType("overload", "typing")); 85 | } 86 | 87 | // MethodRenderer adds warnings.warn() to non-overloaded deprecated methods 88 | if (Methods.Any(m => m.DeprecationReason != null && !m.Overload)) 89 | { 90 | types.Add(new PythonType("warn", "warnings")); 91 | } 92 | 93 | foreach (var innerClass in InnerClasses) 94 | { 95 | foreach (var usedType in innerClass.GetUsedTypes()) 96 | { 97 | types.Add(usedType); 98 | } 99 | } 100 | 101 | return types; 102 | } 103 | 104 | public override string ToString() 105 | { 106 | return Type.ToString(); 107 | } 108 | 109 | /// 110 | /// Returns the used types which need to be recursively iterated over in GetUsedTypes(). 111 | /// 112 | private IEnumerable GetUsedTypesToIterateOver() 113 | { 114 | yield return Type; 115 | 116 | foreach (var inheritedType in InheritsFrom) 117 | { 118 | yield return inheritedType; 119 | } 120 | 121 | if (MetaClass != null) 122 | { 123 | yield return MetaClass; 124 | } 125 | 126 | foreach (var property in Properties) 127 | { 128 | if (property.Type != null) 129 | { 130 | yield return property.Type; 131 | } 132 | } 133 | 134 | foreach (var method in Methods) 135 | { 136 | yield return method.ReturnType; 137 | 138 | foreach (var parameter in method.Parameters) 139 | { 140 | yield return parameter.Type; 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/PropertyRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.IO; 19 | using System.Linq; 20 | using QuantConnectStubsGenerator.Model; 21 | using QuantConnectStubsGenerator.Utility; 22 | 23 | namespace QuantConnectStubsGenerator.Renderer 24 | { 25 | public class PropertyRenderer : ObjectRenderer 26 | { 27 | public PropertyRenderer(TextWriter writer, int indentationLevel, ParseContext context) 28 | : base(writer, indentationLevel, context) 29 | { 30 | } 31 | 32 | public override void Render(Property property) 33 | { 34 | var snakeCasedProperty = GetSnakeCasedProperty(property); 35 | if (snakeCasedProperty != null) 36 | { 37 | property = snakeCasedProperty; 38 | } 39 | 40 | if (ShouldSkip(property)) 41 | { 42 | return; 43 | } 44 | 45 | if (property.Static) 46 | { 47 | RenderAttribute(property); 48 | } 49 | else 50 | { 51 | RenderProperty(property); 52 | } 53 | } 54 | 55 | private bool ShouldSkip(Property property) 56 | { 57 | // Python.Net will favor snake-cased methods over properties, 58 | // so we skip properties what match an existing method's name 59 | 60 | return property.Class.GetClassAndBaseClasses(Context).Any(cls => cls.Methods.Any(method => method.Name == property.Name)); 61 | } 62 | 63 | private static Property GetSnakeCasedProperty(Property property) 64 | { 65 | var name = property.Name.ToSnakeCase(property.Constant); 66 | if (name.IsPythonReservedWord()) 67 | { 68 | return null; 69 | } 70 | 71 | return new Property(property, name); 72 | } 73 | 74 | private void RenderAttribute(Property property) 75 | { 76 | // Some attributes have names in C# that are illegal in Python 77 | if (property.Name == "None" || property.Name == "True" || property.Name == "False") 78 | { 79 | Write("# Cannot convert to Python: "); 80 | } 81 | 82 | Write(property.Name); 83 | 84 | if (property.Type != null) 85 | { 86 | Write($": {property.Type.ToPythonString()}"); 87 | } 88 | 89 | if (property.Value != null) 90 | { 91 | Write($" = {property.Value}"); 92 | } 93 | 94 | WriteLine(); 95 | 96 | WriteSummary(property.Summary); 97 | WriteLine(); 98 | } 99 | 100 | private void RenderProperty(Property property) 101 | { 102 | // Some properties have names starting with "@", which is invalid in Python 103 | if (property.Name.StartsWith("@")) 104 | { 105 | WriteLine($"# Cannot convert property {property.Name} to Python"); 106 | WriteLine(); 107 | return; 108 | } 109 | 110 | WriteLine("@property"); 111 | 112 | if (property.Abstract) 113 | { 114 | WriteLine("@abc.abstractmethod"); 115 | } 116 | 117 | // Add the getter 118 | WriteLine($"def {property.Name}(self) -> {property.Type.ToPythonString()}:"); 119 | WriteSummary(property.Summary, true); 120 | 121 | if (property.DeprecationReason != null) 122 | { 123 | WriteLine($"warnings.warn(\"{property.DeprecationReason}\", DeprecationWarning)".Indent()); 124 | } 125 | else 126 | { 127 | WriteLine("...".Indent()); 128 | } 129 | 130 | WriteLine(); 131 | 132 | // render setter for mypy to be happy 133 | if (property.HasSetter) 134 | { 135 | WriteLine($"@{property.Name}.setter"); 136 | 137 | // Add the getter 138 | WriteLine($"def {property.Name}(self, value: {property.Type.ToPythonString()}) -> None:"); 139 | if (property.DeprecationReason != null) 140 | { 141 | WriteLine($"warnings.warn(\"{property.DeprecationReason}\", DeprecationWarning)".Indent()); 142 | } 143 | else 144 | { 145 | WriteLine("...".Indent()); 146 | } 147 | 148 | WriteLine(); 149 | } 150 | } 151 | } 152 | } 153 | 154 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/Model/ClassTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | using NUnit.Framework; 19 | using QuantConnectStubsGenerator.Model; 20 | 21 | namespace QuantConnectStubsGenerator.Tests.Model 22 | { 23 | [TestFixture] 24 | public class ClassTests 25 | { 26 | [Test] 27 | public void GetUsedTypesShouldReturnAllTypesInTheClassAndItsInnerClasses() 28 | { 29 | var parentCls = new Class(new PythonType("ParentClass", "QuantConnect")); 30 | var childCls = new Class(new PythonType("ChildClass", "QuantConnect")) 31 | { 32 | ParentClass = parentCls, 33 | MetaClass = new PythonType("ABCMeta", "abc") 34 | }; 35 | 36 | parentCls.InnerClasses.Add(childCls); 37 | 38 | childCls.Type.TypeParameters.Add(new PythonType("ChildClass.T", "QuantConnect") 39 | { 40 | IsNamedTypeParameter = true 41 | }); 42 | 43 | childCls.InheritsFrom.Add(PythonType.Any); 44 | 45 | var usedTypes = parentCls.GetUsedTypes().ToList(); 46 | 47 | Assert.AreEqual(7, usedTypes.Count); 48 | AssertTypeExists(usedTypes, "QuantConnect", "ParentClass"); 49 | AssertTypeExists(usedTypes, "QuantConnect", "ChildClass"); 50 | AssertTypeExists(usedTypes, "QuantConnect", "ChildClass.T"); 51 | AssertTypeExists(usedTypes, "typing", "Generic"); 52 | AssertTypeExists(usedTypes, "typing", "TypeVar"); 53 | AssertTypeExists(usedTypes, "typing", "Any"); 54 | AssertTypeExists(usedTypes, "abc", "ABCMeta"); 55 | } 56 | 57 | [Test] 58 | public void GetUsedTypesShouldReturnAllTypesInTheClassAndItsNonStaticProperties() 59 | { 60 | var cls = new Class(new PythonType("MyClass", "QuantConnect")); 61 | 62 | cls.Properties.Add(new Property("Property1") 63 | { 64 | Type = new PythonType("MyProperty", "QuantConnect"), 65 | Abstract = true 66 | }); 67 | 68 | var usedTypes = cls.GetUsedTypes().ToList(); 69 | 70 | Assert.AreEqual(3, usedTypes.Count); 71 | AssertTypeExists(usedTypes, "QuantConnect", "MyClass"); 72 | AssertTypeExists(usedTypes, "QuantConnect", "MyProperty"); 73 | AssertTypeExists(usedTypes, "abc", "abstractmethod"); 74 | } 75 | 76 | [Test] 77 | public void GetUsedTypesShouldReturnAllTypesInTheClassAndItsStaticProperties() 78 | { 79 | var cls = new Class(new PythonType("MyClass", "QuantConnect")); 80 | 81 | cls.Properties.Add(new Property("Property1") 82 | { 83 | Type = new PythonType("MyProperty", "QuantConnect"), 84 | Abstract = true, 85 | Static = true 86 | }); 87 | 88 | var usedTypes = cls.GetUsedTypes().ToList(); 89 | 90 | Assert.AreEqual(2, usedTypes.Count); 91 | AssertTypeExists(usedTypes, "QuantConnect", "MyClass"); 92 | AssertTypeExists(usedTypes, "QuantConnect", "MyProperty"); 93 | } 94 | 95 | [Test] 96 | public void GetUsedTypesShouldReturnAllTypesInTheClassAndItsMethods() 97 | { 98 | var cls = new Class(new PythonType("MyClass", "QuantConnect")); 99 | 100 | cls.Methods.Add(new Method("Method1", new PythonType("ReturnType", "QuantConnect")) 101 | { 102 | Overload = true, 103 | Static = true, 104 | Parameters = 105 | { 106 | new Parameter("parameter1", new PythonType("Parameter1", "QuantConnect")), 107 | new Parameter("parameter2", new PythonType("Parameter2", "QuantConnect")), 108 | new Parameter("parameter3", new PythonType("Parameter3", "QuantConnect")) 109 | } 110 | }); 111 | 112 | var usedTypes = cls.GetUsedTypes().ToList(); 113 | 114 | Assert.AreEqual(6, usedTypes.Count); 115 | AssertTypeExists(usedTypes, "QuantConnect", "MyClass"); 116 | AssertTypeExists(usedTypes, "QuantConnect", "ReturnType"); 117 | AssertTypeExists(usedTypes, "QuantConnect", "Parameter1"); 118 | AssertTypeExists(usedTypes, "QuantConnect", "Parameter2"); 119 | AssertTypeExists(usedTypes, "QuantConnect", "Parameter3"); 120 | AssertTypeExists(usedTypes, "typing", "overload"); 121 | } 122 | 123 | private void AssertTypeExists(IEnumerable types, string ns, string name) 124 | { 125 | Assert.IsTrue(types.Any(t => t.Namespace == ns && t.Name == name)); 126 | } 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Model/PythonType.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Linq; 18 | using System.Collections.Generic; 19 | 20 | namespace QuantConnectStubsGenerator.Model 21 | { 22 | public class PythonType : IEquatable 23 | { 24 | public static readonly PythonType SymbolType = new PythonType("Symbol", "QuantConnect"); 25 | public static readonly PythonType ImplicitConversionParameterSymbolType = 26 | CreateUnion(SymbolType, new PythonType("str"), new PythonType("BaseContract", "QuantConnect.Data.Market")); 27 | 28 | public static readonly PythonType Any = new PythonType("Any", "typing"); 29 | public static readonly PythonType None = new PythonType("None"); 30 | 31 | public string Name { get; set; } 32 | public string Namespace { get; set; } 33 | 34 | public string Alias { get; set; } 35 | public bool IsNamedTypeParameter { get; set; } 36 | 37 | public IList TypeParameters { get; set; } = new List(); 38 | 39 | public PythonType(string name, string ns = null) 40 | { 41 | Name = name; 42 | Namespace = ns; 43 | } 44 | 45 | public PythonType(PythonType other) 46 | : this(other.Name, other.Namespace) 47 | { 48 | Alias = other.Alias; 49 | IsNamedTypeParameter = other.IsNamedTypeParameter; 50 | TypeParameters = new List(other.TypeParameters); 51 | } 52 | 53 | public string GetBaseName() 54 | { 55 | return Name.Contains('.') ? Name.Substring(0, Name.IndexOf('.')) : Name; 56 | } 57 | 58 | public string ToPythonString(bool ignoreAlias = false) 59 | { 60 | if (!ignoreAlias && Alias != null) 61 | { 62 | return Alias; 63 | } 64 | 65 | if (IsNamedTypeParameter) 66 | { 67 | return $"{Namespace}_{Name}".Replace('.', '_'); 68 | } 69 | 70 | var str = ""; 71 | 72 | if (Namespace != null) 73 | { 74 | str += $"{Namespace}."; 75 | } 76 | 77 | str += Name; 78 | 79 | if (TypeParameters.Count == 0) 80 | { 81 | return str; 82 | } 83 | 84 | str += "["; 85 | 86 | // Callable requires Callable[[ParameterType1, ParameterType2, ...], ReturnType] 87 | if (Namespace == "typing" && Name == "Callable") 88 | { 89 | str += "["; 90 | str += string.Join( 91 | ", ", 92 | TypeParameters.SkipLast(1).Select(type => type.ToPythonString())); 93 | str += "], "; 94 | str += TypeParameters.Last().ToPythonString(); 95 | } 96 | else 97 | { 98 | str += string.Join(", ", TypeParameters.Select(type => type.ToPythonString())); 99 | } 100 | 101 | str += "]"; 102 | 103 | return str; 104 | } 105 | 106 | public override string ToString() 107 | { 108 | return ToPythonString(); 109 | } 110 | 111 | public bool Equals(PythonType other) 112 | { 113 | if (ReferenceEquals(null, other)) return false; 114 | if (ReferenceEquals(this, other)) return true; 115 | return Name == other.Name 116 | && Namespace == other.Namespace 117 | && Alias == other.Alias 118 | && IsNamedTypeParameter == other.IsNamedTypeParameter 119 | && TypeParameters.SequenceEqual(other.TypeParameters); 120 | } 121 | 122 | public override bool Equals(object obj) 123 | { 124 | if (ReferenceEquals(null, obj)) return false; 125 | if (ReferenceEquals(this, obj)) return true; 126 | return obj.GetType() == GetType() && Equals((PythonType)obj); 127 | } 128 | 129 | public override int GetHashCode() 130 | { 131 | var hash = 19; 132 | foreach (var type in TypeParameters) 133 | { 134 | hash = hash * 31 + type.GetHashCode(); 135 | } 136 | 137 | return HashCode.Combine(Name, Namespace, Alias, IsNamedTypeParameter, hash); 138 | } 139 | 140 | public static PythonType CreateUnion(params PythonType[] pythonTypes) 141 | { 142 | var unionType = new PythonType("Union", "typing"); 143 | foreach (var pythonType in pythonTypes ?? Enumerable.Empty()) 144 | { 145 | unionType.TypeParameters.Add(pythonType); 146 | } 147 | return unionType; 148 | } 149 | } 150 | } 151 | 152 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/SetupRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | using System.IO; 19 | using System.Linq; 20 | using log4net; 21 | 22 | namespace QuantConnectStubsGenerator.Renderer 23 | { 24 | public class SetupRenderer : BaseRenderer 25 | { 26 | private static readonly ILog Logger = LogManager.GetLogger(typeof(SetupRenderer)); 27 | 28 | private readonly string _leanPath; 29 | private readonly string _outputDirectory; 30 | 31 | public SetupRenderer(TextWriter writer, string leanPath, string outputDirectory) : base(writer) 32 | { 33 | _leanPath = leanPath; 34 | _outputDirectory = outputDirectory; 35 | } 36 | 37 | public void Render() 38 | { 39 | var packageVersion = GetPackageVersion(); 40 | var namespaces = GetNamespaces(); 41 | 42 | WriteLine($@" 43 | from setuptools import setup 44 | 45 | long_description = """""" 46 | # QuantConnect Stubs 47 | 48 | This package contains type stubs for QuantConnect's [Lean](https://github.com/QuantConnect/Lean) algorithmic trading engine and for parts of the .NET library that are used by Lean. 49 | 50 | These stubs can be used by editors to provide type-aware features like autocomplete and auto-imports in QuantConnect strategies written in Python. 51 | 52 | After installing the stubs, you can copy the following line to the top of every Python file to have the same imports as the ones that are added by default in the cloud: 53 | ```py 54 | from AlgorithmImports import * 55 | ``` 56 | 57 | This line imports [all common QuantConnect members](https://github.com/QuantConnect/Lean/blob/master/Common/AlgorithmImports.py) and provides autocomplete for them. 58 | """""".strip() 59 | 60 | setup( 61 | name=""quantconnect-stubs"", 62 | version=""{packageVersion}"", 63 | description=""Type stubs for QuantConnect's Lean"", 64 | author=""QuantConnect"", 65 | author_email=""support@quantconnect.com"", 66 | url=""https://github.com/QuantConnect/quantconnect-stubs-generator"", 67 | long_description=long_description, 68 | long_description_content_type=""text/markdown"", 69 | classifiers=[ 70 | ""Development Status :: 5 - Production/Stable"", 71 | ""Intended Audience :: Developers"", 72 | ""Intended Audience :: Financial and Insurance Industry"", 73 | ""License :: OSI Approved :: Apache Software License"", 74 | ""Programming Language :: Python :: 3"" 75 | ], 76 | install_requires=[""pandas"", ""matplotlib""], 77 | packages=[ 78 | {string.Join(",\n", namespaces.Select(ns => new string(' ', 8) + $"\"{ns}\""))} 79 | ], 80 | package_data={{ 81 | {string.Join(",\n", namespaces.Select(ns => new string(' ', 8) + $"\"{ns}\": [\"*.py\", \"*.pyi\", \"py.typed\"]"))} 82 | }} 83 | ) 84 | ".Trim()); 85 | } 86 | 87 | private string GetPackageVersion() 88 | { 89 | if (Environment.GetEnvironmentVariables().Contains("STUBS_VERSION")) 90 | { 91 | return Environment.GetEnvironmentVariables()["STUBS_VERSION"]?.ToString(); 92 | } 93 | 94 | const string defaultVersion = "1.0.0"; 95 | var tagsDirectory = new DirectoryInfo(Path.GetFullPath(".git/refs/tags", _leanPath)); 96 | 97 | if (!tagsDirectory.Exists) 98 | { 99 | Logger.Warn($"Provided Lean path is not a Git repository, setting version to {defaultVersion}"); 100 | return defaultVersion; 101 | } 102 | 103 | var files = tagsDirectory 104 | .GetFiles() 105 | .Select(file => file.Name) 106 | .Select(name => int.TryParse(name, out var n) ? n : (int?)null) 107 | .Where(tag => tag.HasValue) 108 | .Select(tag => tag.Value) 109 | .OrderBy(tag => tag) 110 | .ToList(); 111 | 112 | if (files.Count != 0) 113 | { 114 | return files.Last().ToString(); 115 | } 116 | 117 | Logger.Warn($"Provided Lean path is not a Git repository with tags, setting version to {defaultVersion}"); 118 | return defaultVersion; 119 | } 120 | 121 | private List GetNamespaces() 122 | { 123 | return Directory.GetFiles(_outputDirectory, "__init__.py*", SearchOption.AllDirectories) 124 | .Select(file => file.Replace('\\', '/')) 125 | .Select(file => 126 | { 127 | var ns = file.Replace(_outputDirectory, "").Substring(1); 128 | ns = ns.Substring(0, ns.Contains('/') ? ns.LastIndexOf('/') : ns.Length); 129 | return ns.Replace('/', '.'); 130 | }).Distinct().OrderBy(name => name).ToList(); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Renderer/MethodRenderer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using System.IO; 18 | using System.Linq; 19 | using System.Text.RegularExpressions; 20 | using QuantConnectStubsGenerator.Model; 21 | using QuantConnectStubsGenerator.Utility; 22 | 23 | namespace QuantConnectStubsGenerator.Renderer 24 | { 25 | public class MethodRenderer : ObjectRenderer 26 | { 27 | public MethodRenderer(TextWriter writer, int indentationLevel, ParseContext context) 28 | : base(writer, indentationLevel, context) 29 | { 30 | } 31 | 32 | public override void Render(Method method) 33 | { 34 | var snakeCasedMethod = GetSnakeCasedMethod(method); 35 | if (snakeCasedMethod != null) 36 | { 37 | method = snakeCasedMethod; 38 | } 39 | 40 | if (method.Static) 41 | { 42 | WriteLine("@staticmethod"); 43 | } 44 | 45 | if (method.Overload) 46 | { 47 | WriteLine("@overload"); 48 | } 49 | 50 | // In C# abstract methods and method overloads can be mixed freely 51 | // In Python this is not the case, overloaded abstract methods or 52 | // overloaded methods of which only some are abstract are not parsed the same in Python 53 | // For this reason @abc.abstractmethod is not added to abstract methods 54 | 55 | var args = new List(); 56 | 57 | if (!method.Static) 58 | { 59 | args.Add("self"); 60 | } 61 | 62 | args.AddRange(method.Parameters.Select(ParameterToString)); 63 | var argsStr = string.Join(", ", args); 64 | 65 | WriteLine($"def {method.Name}({argsStr}) -> {method.ReturnType.ToPythonString()}:"); 66 | WriteSummary(method.Summary, true); 67 | 68 | // PyCharm has several issues with warnings.warn() calls in overloaded methods 69 | // 70 | // Example 1: 71 | // def my_method() -> None: warnings.warn("Reason 1") 72 | // def my_method(var: int) -> None: warnings.warn("Reason 2") 73 | // 74 | // Example 2: 75 | // def my_method() -> None: warnings.warn("Reason 1") 76 | // def my_method(var: int) -> None: ... 77 | // 78 | // In both examples PyCharm will flag "my_method(2)" as being deprecated with message "Reason 1" 79 | // We therefore only add warnings.warn() to non-overloaded deprecated methods 80 | if (method.DeprecationReason != null && !method.Overload) 81 | { 82 | WriteLine($"warnings.warn(\"{method.DeprecationReason}\", DeprecationWarning)".Indent()); 83 | } 84 | else 85 | { 86 | WriteLine("...".Indent()); 87 | } 88 | 89 | WriteLine(); 90 | } 91 | 92 | private string ParameterToString(Parameter parameter) 93 | { 94 | var str = $"{parameter.Name}: {parameter.Type.ToPythonString()}"; 95 | 96 | if (parameter.VarArgs) 97 | { 98 | str = "*" + str; 99 | } 100 | 101 | if (parameter.Value != null) 102 | { 103 | str += $" = {parameter.Value}"; 104 | } 105 | 106 | return str; 107 | } 108 | 109 | private static Method GetSnakeCasedMethod(Method method) 110 | { 111 | var snakeCasedMethodName = method.Name.ToSnakeCase(); 112 | if (snakeCasedMethodName.IsPythonReservedWord()) 113 | { 114 | return null; 115 | } 116 | 117 | var snakeCasedMethod = new Method(snakeCasedMethodName, method.ReturnType) 118 | { 119 | Static = method.Static, 120 | Overload = method.Overload, 121 | File = method.File, 122 | DeprecationReason = method.DeprecationReason, 123 | Class = method.Class 124 | }; 125 | 126 | var summary = method.Summary; 127 | 128 | foreach (var parameter in method.Parameters) 129 | { 130 | if (parameter.Name.StartsWith("*")) 131 | { 132 | return method; 133 | } 134 | var snakeCasedParameterName = parameter.Name.ToSnakeCase(); 135 | if (snakeCasedParameterName.IsPythonReservedWord()) 136 | { 137 | return null; 138 | } 139 | 140 | snakeCasedMethod.Parameters.Add(new Parameter(snakeCasedParameterName, parameter.Type) 141 | { 142 | VarArgs = parameter.VarArgs, 143 | Value = parameter.Value 144 | }); 145 | 146 | if (summary != null) 147 | { 148 | summary = Regex.Replace(summary, @$"\b{parameter.Name}\b", snakeCasedParameterName); 149 | } 150 | } 151 | 152 | snakeCasedMethod.Summary = summary; 153 | 154 | return snakeCasedMethod; 155 | } 156 | } 157 | } 158 | 159 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/MethodParserTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2025 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | using NUnit.Framework; 19 | using NUnit.Framework.Internal; 20 | using QuantConnectStubsGenerator.Model; 21 | using static QuantConnectStubsGenerator.Tests.GeneratorTests; 22 | 23 | namespace QuantConnectStubsGenerator.Tests 24 | { 25 | [TestFixture] 26 | public class MethodParserTests 27 | { 28 | [Test] 29 | public void ParamsHandling() 30 | { 31 | var testGenerator = new TestGenerator 32 | { 33 | Files = new() 34 | { 35 | { "Test.cs", @" 36 | namespace QuantConnect.MethodParserTests 37 | { 38 | public class TestClass 39 | { 40 | public int ParamsTestMethod(params string[] tickers) 41 | { 42 | return 1; 43 | } 44 | } 45 | }" } 46 | } 47 | }; 48 | 49 | var result = testGenerator.GenerateModelsPublic(); 50 | 51 | var namespaces = result.GetNamespaces().ToList(); 52 | Assert.AreEqual(2, namespaces.Count); 53 | 54 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 55 | var testNameSpace = namespaces.Single(x => x.Name == "QuantConnect.MethodParserTests"); 56 | 57 | var testClass = testNameSpace.GetClasses().Single(); 58 | Assert.AreEqual("TestClass", testClass.Type.Name); 59 | 60 | var testMethodCount = testClass.Methods.Count; 61 | Assert.AreEqual(1, testMethodCount); 62 | 63 | var method = testClass.Methods.Single(); 64 | 65 | Assert.AreEqual(1, method.Parameters.Count); 66 | Assert.AreEqual("Union", method.Parameters[0].Type.Name); 67 | } 68 | 69 | [Test] 70 | public void CallableMethodsAcceptLambdas() 71 | { 72 | var testGenerator = new TestGenerator 73 | { 74 | Files = new() 75 | { 76 | { "Test.cs", @" 77 | using System; 78 | 79 | namespace QuantConnect.MethodParserTests 80 | { 81 | public class TestClass 82 | { 83 | public void TestMethod(Action handler) 84 | { 85 | } 86 | } 87 | }" } 88 | } 89 | }; 90 | 91 | var result = testGenerator.GenerateModelsPublic(); 92 | 93 | var namespaces = result.GetNamespaces().ToList(); 94 | Assert.AreEqual(2, namespaces.Count); 95 | 96 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 97 | var testNameSpace = namespaces.Single(x => x.Name == "QuantConnect.MethodParserTests"); 98 | var testClass = testNameSpace.GetClasses().Single(); 99 | 100 | var method = testClass.Methods.Single(); 101 | 102 | Assert.AreEqual("TestMethod", method.Name); 103 | 104 | Assert.AreEqual(1, method.Parameters.Count); 105 | var parameter = method.Parameters[0]; 106 | Assert.AreEqual("Callable", parameter.Type.Name); 107 | Assert.AreEqual("typing", parameter.Type.Namespace); 108 | 109 | Assert.AreEqual(2, parameter.Type.TypeParameters.Count); 110 | Assert.AreEqual("str", parameter.Type.TypeParameters[0].Name); 111 | Assert.IsNull(parameter.Type.TypeParameters[0].Namespace); 112 | Assert.AreEqual("Any", parameter.Type.TypeParameters[1].Name); 113 | Assert.AreEqual("typing", parameter.Type.TypeParameters[1].Namespace); 114 | } 115 | 116 | private static IEnumerable GetPythonnetTypesTestCases 117 | { 118 | get 119 | { 120 | yield return new TestCaseData("PyObject", PythonType.Any); 121 | yield return new TestCaseData("PyList", new PythonType("List", "typing") { TypeParameters = [PythonType.Any] }); 122 | yield return new TestCaseData("PyDict", new PythonType("Dict", "typing") { TypeParameters = new List{ PythonType.Any, PythonType.Any } }); 123 | } 124 | } 125 | 126 | [TestCaseSource(nameof(GetPythonnetTypesTestCases))] 127 | public void PythonnetTypeParametersAreConvertedToTypingAny(string pythonnetType, PythonType expectedConvertedType) 128 | { 129 | var testGenerator = new TestGenerator 130 | { 131 | Files = new() 132 | { 133 | { "Test.cs", @$" 134 | using System; 135 | using System.Collections.Generic; 136 | 137 | namespace QuantConnect.MethodParserTests 138 | {{ 139 | public class TestClass 140 | {{ 141 | public void TestMethod1({pythonnetType} arg) 142 | {{ 143 | }} 144 | public void TestMethod2(List<{pythonnetType}> arg) 145 | {{ 146 | }} 147 | public void TestMethod3(params {pythonnetType}[] args) 148 | {{ 149 | }} 150 | public {pythonnetType} TestMethod4() 151 | {{ 152 | }} 153 | }} 154 | }}" } 155 | } 156 | }; 157 | 158 | var result = testGenerator.GenerateModelsPublic(); 159 | var namespaces = result.GetNamespaces().ToList(); 160 | 161 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 162 | var testNameSpace = namespaces.Single(x => x.Name == "QuantConnect.MethodParserTests"); 163 | var testClass = testNameSpace.GetClasses().Single(); 164 | 165 | var method1 = testClass.Methods.Single(x => x.Name == "TestMethod1"); 166 | Assert.AreEqual(1, method1.Parameters.Count); 167 | Assert.AreEqual(expectedConvertedType, method1.Parameters[0].Type); 168 | 169 | var method2 = testClass.Methods.Single(x => x.Name == "TestMethod2"); 170 | Assert.AreEqual(1, method2.Parameters.Count); 171 | Assert.AreEqual(new PythonType("List", "typing") { TypeParameters = [expectedConvertedType] }, method2.Parameters[0].Type); 172 | 173 | var method3 = testClass.Methods.Single(x => x.Name == "TestMethod3"); 174 | Assert.AreEqual(1, method3.Parameters.Count); 175 | Assert.AreEqual(new PythonType("Union", "typing") { TypeParameters = [expectedConvertedType, new PythonType("Iterable", "typing") { TypeParameters = [expectedConvertedType] }] }, method3.Parameters[0].Type); 176 | Assert.IsTrue(method3.Parameters[0].VarArgs); 177 | 178 | var method4 = testClass.Methods.Single(x => x.Name == "TestMethod4"); 179 | Assert.AreEqual(expectedConvertedType, method4.ReturnType); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Parser/ClassParser.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | using Microsoft.CodeAnalysis; 19 | using Microsoft.CodeAnalysis.CSharp; 20 | using Microsoft.CodeAnalysis.CSharp.Syntax; 21 | using QuantConnectStubsGenerator.Model; 22 | 23 | namespace QuantConnectStubsGenerator.Parser 24 | { 25 | public class ClassParser : BaseParser 26 | { 27 | public ClassParser(ParseContext context, SemanticModel model) : base(context, model) 28 | { 29 | } 30 | 31 | protected override void EnterClass(BaseTypeDeclarationSyntax node) 32 | { 33 | var cls = ParseClass(node); 34 | 35 | if (_currentNamespace.HasClass(cls.Type)) 36 | { 37 | var existingClass = _currentNamespace.GetClassByType(cls.Type); 38 | 39 | // Some classes in C# exist multiple times with varying amounts of generics 40 | // We keep the one with the most generics 41 | if (existingClass.Type.TypeParameters.Count >= cls.Type.TypeParameters.Count) 42 | { 43 | // Add documentation if the existing class has been registered without it and it is available here 44 | existingClass.Summary ??= cls.Summary; 45 | 46 | _currentClass = existingClass; 47 | return; 48 | } 49 | } 50 | 51 | if (_currentClass != null) 52 | { 53 | cls.ParentClass = _currentClass; 54 | _currentClass.InnerClasses.Add(cls); 55 | } 56 | 57 | _currentNamespace.RegisterClass(cls); 58 | _currentClass = cls; 59 | } 60 | 61 | private Class ParseClass(BaseTypeDeclarationSyntax node) 62 | { 63 | return new Class(_typeConverter.GetType(node, true, true, false)) 64 | { 65 | Static = HasModifier(node, "static"), 66 | Documentation = GetXmlDocumentation(node, CodeEntityType.Class), 67 | Interface = node is InterfaceDeclarationSyntax, 68 | InheritsFrom = ParseInheritedTypes(node).ToList(), 69 | MetaClass = ParseMetaClass(node), 70 | AvoidImplicitTypes = HasAttribute(node.AttributeLists, "StubsAvoidImplicits") 71 | }; 72 | } 73 | 74 | private IEnumerable ParseInheritedTypes(BaseTypeDeclarationSyntax node) 75 | { 76 | var types = new List(); 77 | 78 | var symbol = _model.GetDeclaredSymbol(node); 79 | if (symbol == null) 80 | { 81 | return types; 82 | } 83 | 84 | var currentType = _typeConverter.GetType(node, true, true, false); 85 | var skipTypeNormalization = !currentType.Namespace.StartsWith("QuantConnect"); 86 | 87 | if (symbol.BaseType != null) 88 | { 89 | var ns = symbol.BaseType.ContainingNamespace.Name; 90 | var name = symbol.BaseType.Name; 91 | 92 | if (!ShouldSkipBaseType(currentType, ns, name)) 93 | { 94 | types.Add(_typeConverter.GetType(symbol.BaseType, skipTypeNormalization: skipTypeNormalization)); 95 | } 96 | } 97 | 98 | // "Cannot create consistent method ordering" errors appear when a Python class 99 | // extends from classes A and B where B extends from A 100 | // In this case we remove the direct inheritance on A 101 | var interfacesToRemove = new HashSet(); 102 | foreach (var typeA in symbol.Interfaces) 103 | { 104 | foreach (var typeB in symbol.Interfaces) 105 | { 106 | if (typeB.Interfaces.Any(x => x.Name == typeA.Name && x.IsGenericType == typeA.IsGenericType)) 107 | { 108 | interfacesToRemove.Add(typeA); 109 | } 110 | } 111 | } 112 | 113 | foreach (var typeSymbol in symbol.Interfaces.Except(interfacesToRemove)) 114 | { 115 | var type = _typeConverter.GetType(typeSymbol, skipTypeNormalization: skipTypeNormalization); 116 | 117 | // In C# a class can be extended multiple times with different amounts of generics 118 | // In Python this is not possible, so we keep the type with the most generics 119 | var existingType = types.FirstOrDefault(t => t.Namespace == type.Namespace && t.Name == type.Name); 120 | if (existingType != null) 121 | { 122 | if (existingType.TypeParameters.Count < type.TypeParameters.Count) 123 | { 124 | existingType.TypeParameters = type.TypeParameters; 125 | } 126 | 127 | continue; 128 | } 129 | 130 | types.Add(type); 131 | } 132 | 133 | // Ensure classes don't extend from both typing.List and typing.Dict, that causes conflicting definitions 134 | var listType = types.FirstOrDefault(type => type.ToPythonString().StartsWith("typing.List[")); 135 | var dictType = types.FirstOrDefault(type => type.ToPythonString().StartsWith("typing.Dict[")); 136 | 137 | if (listType != null && dictType != null) 138 | { 139 | types.Remove(listType); 140 | } 141 | 142 | types = types.Select(type => ValidateInheritedType(currentType, type)).ToList(); 143 | 144 | return types; 145 | } 146 | 147 | private PythonType ParseMetaClass(BaseTypeDeclarationSyntax node) 148 | { 149 | if (node is InterfaceDeclarationSyntax || HasModifier(node, "abstract")) 150 | { 151 | return new PythonType("ABCMeta", "abc"); 152 | } 153 | 154 | return null; 155 | } 156 | 157 | private PythonType ValidateInheritedType(PythonType currentType, PythonType inheritedType) 158 | { 159 | if (inheritedType.IsNamedTypeParameter) 160 | { 161 | return inheritedType; 162 | } 163 | 164 | // Python classes can't reference themselves or any of their parent classes in their inherited types 165 | if (currentType.GetBaseName() == inheritedType.GetBaseName() 166 | && currentType.Namespace == inheritedType.Namespace) 167 | { 168 | return ToAnyAlias(inheritedType); 169 | } 170 | 171 | inheritedType.TypeParameters = inheritedType.TypeParameters 172 | .Select(type => ValidateInheritedType(currentType, type)) 173 | .ToList(); 174 | 175 | return inheritedType; 176 | } 177 | 178 | private PythonType ToAnyAlias(PythonType type) 179 | { 180 | var alias = type.Name.Replace('.', '_'); 181 | if (type.Namespace != null) 182 | { 183 | alias = $"{type.Namespace.Replace('.', '_')}_{alias}"; 184 | } 185 | 186 | return new PythonType("Any", "typing") 187 | { 188 | Alias = alias 189 | }; 190 | } 191 | 192 | private bool ShouldSkipBaseType(PythonType currentType, string ns, string name) 193 | { 194 | // System.Object extends from System.Object in the AST, we skip this base type in Python 195 | if (currentType.Namespace == "System" && currentType.Name == "Object" && ns == "System" && name == "Object") 196 | { 197 | return true; 198 | } 199 | 200 | // We don't parse a ValueType, so we can't extend from it without errors 201 | return ns == "System" && name == "ValueType"; 202 | } 203 | } 204 | } 205 | 206 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Utility/XmlExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Linq; 17 | using System.Net; 18 | using System.Runtime.CompilerServices; 19 | using System.Xml; 20 | using QuantConnectStubsGenerator.Model; 21 | 22 | namespace QuantConnectStubsGenerator.Utility 23 | { 24 | public static class XmlExtensions 25 | { 26 | public static string GetText(this XmlElement element, CodeEntity entity, ParseContext context, bool adjustNames = true) 27 | { 28 | var clone = element.CloneNode(true); 29 | 30 | for (int i = 0, iMax = clone.ChildNodes.Count; i < iMax; i++) 31 | { 32 | var child = clone.ChildNodes[i]; 33 | string newText = null; 34 | 35 | switch (child.Name) 36 | { 37 | case "paramref": 38 | case "typeparamref": 39 | newText = adjustNames ? child.Attributes["name"].InnerText.ToSnakeCase() : child.Attributes["name"].InnerText; 40 | break; 41 | case "see": 42 | case "seealso": 43 | { 44 | // Replace cref, paramref, langword and href tags with their content 45 | var attribute = child.Attributes["langword"] ?? child.Attributes["href"]; 46 | if (attribute != null) 47 | { 48 | newText = attribute.InnerText; 49 | } 50 | else if ((attribute = child.Attributes["paramref"]) != null) 51 | { 52 | newText = adjustNames ? attribute.InnerText.ToSnakeCase() : attribute.InnerText; 53 | } 54 | else if ((attribute = child.Attributes["cref"]) != null) 55 | { 56 | newText = attribute.InnerText; 57 | 58 | if (adjustNames) 59 | { 60 | var parts = newText.Split('.'); 61 | var name = parts[^1]; 62 | var parenthesisIndex = parts[^1].IndexOf('('); 63 | var isMethod = false; 64 | if (parenthesisIndex != -1) 65 | { 66 | parts[^1] = name = name.Substring(0, parenthesisIndex); 67 | isMethod = true; 68 | } 69 | 70 | var referencedEntity = GetCodeEntity(name, newText, parts, entity, context, isMethod); 71 | if (referencedEntity != null) 72 | { 73 | if (referencedEntity is Method) 74 | { 75 | newText = newText.Replace(name, name.ToSnakeCase()); 76 | } 77 | else if (referencedEntity is Property property) 78 | { 79 | newText = newText.Replace(name, name.ToSnakeCase(constant: property.Constant)); 80 | } 81 | } 82 | // else: null or a class, assume it's a class, no need to change anything 83 | } 84 | } 85 | else 86 | { 87 | newText = child.InnerText; 88 | } 89 | 90 | // Convert "T:System.Object" to "System.Object" 91 | if (newText.Length > 2 && newText[1] == ':') 92 | { 93 | newText = newText.Substring(2); 94 | } 95 | 96 | break; 97 | } 98 | } 99 | 100 | if (newText == null) 101 | { 102 | continue; 103 | } 104 | 105 | var newNode = clone.OwnerDocument.CreateTextNode(newText); 106 | clone.ReplaceChild(newNode, child); 107 | } 108 | 109 | var text = clone.InnerText.Trim(); 110 | 111 | // Escape backslashes 112 | text = text.Replace("\\", "\\\\"); 113 | 114 | // Decode HTML entities 115 | text = WebUtility.HtmlDecode(text); 116 | 117 | return text; 118 | } 119 | 120 | /// 121 | /// This will try to find the referenced entity (class, method or property/field) in the XML documentation. 122 | /// It will first look in the current class, then in the imported namespaces. 123 | /// 124 | private static CodeEntity GetCodeEntity(string name, string fullName, string[] parts, CodeEntity entity, ParseContext context, bool isMethod) 125 | { 126 | if (string.IsNullOrEmpty(name) || context == null) 127 | { 128 | return null; 129 | } 130 | 131 | var entityClass = entity switch 132 | { 133 | Class c => c, 134 | Property p => p.Class, 135 | Method m => m.Class, 136 | }; 137 | 138 | if (parts.Length == 1) 139 | { 140 | // Could be either a method or a property of the class itself 141 | var method = entityClass.Methods.FirstOrDefault(x => x.Name == parts[0]); 142 | if (method != null) 143 | { 144 | return method; 145 | } 146 | 147 | var property = entityClass.Properties.FirstOrDefault(x => x.Name == parts[0]); 148 | if (property != null) 149 | { 150 | return property; 151 | } 152 | 153 | // It's a class, let's try to find it 154 | var ns = context.GetNamespaceByName(entityClass.Type.Namespace); 155 | return context.GetNamespaces() 156 | .Where(x => x.Name.StartsWith("QuantConnect") && IsInNamespace(x, ns)) 157 | .SelectMany(x => x.GetClasses()) 158 | .FirstOrDefault(x => x.Type.Name == fullName); 159 | } 160 | 161 | var entityNamespace = context.GetNamespaceByName(entityClass.Type.Namespace); 162 | 163 | foreach (var ns in context.GetNamespaces().Where(x => x.Name.StartsWith("QuantConnect"))) 164 | { 165 | foreach (var cls in ns.GetClasses()) 166 | { 167 | foreach (var method in cls.Methods) 168 | { 169 | if (method.Name == name && BelongsToClass(fullName, cls) && IsInNamespace(ns, entityNamespace)) 170 | { 171 | return method; 172 | } 173 | } 174 | 175 | // Not a method 176 | if (!isMethod) 177 | { 178 | // Could be a property of the current class 179 | foreach (var property in cls.Properties) 180 | { 181 | if (property.Name == name && BelongsToClass(fullName, cls) && IsInNamespace(ns, entityNamespace)) 182 | { 183 | return property; 184 | } 185 | } 186 | 187 | // Not a property, could be the current class itself 188 | if (cls.Type.Name == fullName && IsInNamespace(ns, entityNamespace)) 189 | { 190 | return cls; 191 | } 192 | } 193 | } 194 | } 195 | 196 | return null; 197 | } 198 | 199 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 200 | private static bool BelongsToClass(string fullName, Class cls) 201 | { 202 | return fullName.Substring(0, fullName.LastIndexOf('.')) == cls.Type.Name; 203 | } 204 | 205 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 206 | private static bool IsInNamespace(Namespace ns, Namespace entityNamespace) 207 | { 208 | return ns.Name == entityNamespace.Name || entityNamespace.NamespacesToImport.Contains(ns.Name); 209 | } 210 | } 211 | } 212 | 213 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Parser/PropertyParser.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Linq; 17 | using Microsoft.CodeAnalysis; 18 | using Microsoft.CodeAnalysis.CSharp; 19 | using Microsoft.CodeAnalysis.CSharp.Syntax; 20 | using QuantConnectStubsGenerator.Model; 21 | using QuantConnectStubsGenerator.Utility; 22 | 23 | namespace QuantConnectStubsGenerator.Parser 24 | { 25 | public class PropertyParser : BaseParser 26 | { 27 | public PropertyParser(ParseContext context, SemanticModel model) : base(context, model) 28 | { 29 | } 30 | 31 | public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) 32 | { 33 | VisitProperty(node, _typeConverter.GetType(node.Type), node.Identifier.Text); 34 | } 35 | 36 | public override void VisitFieldDeclaration(FieldDeclarationSyntax node) 37 | { 38 | VisitField(node, _typeConverter.GetType(node.Declaration.Type)); 39 | } 40 | 41 | public override void VisitEventDeclaration(EventDeclarationSyntax node) 42 | { 43 | CreateEventContainerIfNecessary(); 44 | 45 | var callableType = _typeConverter.GetType(node.Type); 46 | var type = new PythonType("_EventContainer") 47 | { 48 | TypeParameters = {callableType, callableType.TypeParameters.Last()} 49 | }; 50 | 51 | VisitProperty(node, type, node.Identifier.Text); 52 | } 53 | 54 | public override void VisitEventFieldDeclaration(EventFieldDeclarationSyntax node) 55 | { 56 | CreateEventContainerIfNecessary(); 57 | 58 | var callableType = _typeConverter.GetType(node.Declaration.Type); 59 | var type = new PythonType("_EventContainer") 60 | { 61 | TypeParameters = {callableType, callableType.TypeParameters.Last()} 62 | }; 63 | 64 | VisitField(node, type); 65 | } 66 | 67 | public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) 68 | { 69 | var property = new Property(node.Identifier.Text) 70 | { 71 | Value = node.EqualsValue != null 72 | ? FormatValue(node.EqualsValue.Value.ToString()) 73 | : _currentClass.Properties.Count.ToString(), 74 | Static = true, 75 | Abstract = _currentClass.Interface || HasModifier(node, "abstract"), 76 | Constant = true, 77 | DeprecationReason = GetDeprecationReason(node), 78 | Class = _currentClass, 79 | Documentation = GetXmlDocumentation(node, CodeEntityType.Property), 80 | }; 81 | 82 | _currentClass.Properties.Add(property); 83 | } 84 | 85 | private void VisitProperty(BasePropertyDeclarationSyntax node, PythonType type, string name) 86 | { 87 | if (ShouldSkip(node)) 88 | { 89 | return; 90 | } 91 | 92 | if (_currentClass == null) 93 | { 94 | return; 95 | } 96 | 97 | if (_currentClass.Properties.Any(p => p.Name == name)) 98 | { 99 | return; 100 | } 101 | 102 | var originalType = type; 103 | 104 | // Security.Data is of type dynamic but can be used like it is of type DynamicSecurityData 105 | if (_currentClass.Type.ToPythonString() == "QuantConnect.Securities.Security" && name == "Data") 106 | { 107 | type = new PythonType("DynamicSecurityData", "QuantConnect.Securities"); 108 | } 109 | 110 | var property = new Property(name) 111 | { 112 | Type = type, 113 | Static = _currentClass.Static || HasModifier(node, "static"), 114 | Abstract = _currentClass.Interface || HasModifier(node, "abstract"), 115 | Constant = IsStaticReadonly(node), 116 | DeprecationReason = GetDeprecationReason(node), 117 | HasSetter = (node.AccessorList != null && node.AccessorList.Accessors.Any(x => x.Keyword.Text == "set" && IsPublic(x.Modifiers))) 118 | || (node is EventDeclarationSyntax eventNode && IsPublic(eventNode.Modifiers)), 119 | Class = _currentClass, 120 | Documentation = GetXmlDocumentation(node, CodeEntityType.Property), 121 | }; 122 | 123 | _currentClass.Properties.Add(property); 124 | } 125 | 126 | private bool IsPublic(SyntaxTokenList modifiers) 127 | { 128 | return !HasModifier(modifiers, "private") && !HasModifier(modifiers, "internal"); 129 | } 130 | 131 | private void VisitField(BaseFieldDeclarationSyntax node, PythonType type) 132 | { 133 | if (ShouldSkip(node)) 134 | { 135 | return; 136 | } 137 | 138 | if (_currentClass == null) 139 | { 140 | return; 141 | } 142 | 143 | foreach (var variable in node.Declaration.Variables) 144 | { 145 | var property = new Property(variable.Identifier.Text) 146 | { 147 | Type = type, 148 | Static = _currentClass.Static || HasModifier(node, "static") || HasModifier(node, "const"), 149 | Abstract = _currentClass.Interface || HasModifier(node, "abstract"), 150 | Constant = HasModifier(node, "const") || (HasModifier(node, "static") && HasModifier(node, "readonly")), 151 | DeprecationReason = GetDeprecationReason(node), 152 | Class = _currentClass, 153 | HasSetter = IsPublic(node.Modifiers), 154 | Documentation = GetXmlDocumentation(node, CodeEntityType.Field), 155 | }; 156 | 157 | if (variable.Initializer != null) 158 | { 159 | property.Value = FormatValue(variable.Initializer.Value.ToString()); 160 | } 161 | 162 | _currentClass.Properties.Add(property); 163 | } 164 | } 165 | 166 | /// 167 | /// This methods generates the _EventContainer class if it doesn't exist yet. 168 | /// This class is used to provide accurate autocomplete on events, 169 | /// containing just the methods Python.NET allows to be called on event properties and fields. 170 | /// 171 | private void CreateEventContainerIfNecessary() 172 | { 173 | var classType = new PythonType("_EventContainer", _currentNamespace.Name) 174 | { 175 | TypeParameters = 176 | { 177 | new PythonType("_EventContainer_Callable", _currentNamespace.Name) 178 | { 179 | IsNamedTypeParameter = true, 180 | }, 181 | new PythonType("_EventContainer_ReturnType", _currentNamespace.Name) 182 | { 183 | IsNamedTypeParameter = true 184 | } 185 | } 186 | }; 187 | 188 | if (_currentNamespace.HasClass(classType)) 189 | { 190 | return; 191 | } 192 | 193 | _currentNamespace.RegisterClass(new Class(classType) 194 | { 195 | Summary = "This class is used to provide accurate autocomplete on events and cannot be imported.", 196 | Methods = 197 | { 198 | new Method("__iadd__", new PythonType("Self", "typing")) 199 | { 200 | Summary = "Registers an event handler.", 201 | Parameters = {new Parameter("item", classType.TypeParameters[0])}, 202 | Class = _currentClass 203 | }, 204 | new Method("__isub__", new PythonType("Self", "typing")) 205 | { 206 | Summary = "Unregisters an event handler.", 207 | Parameters = {new Parameter("item", classType.TypeParameters[0])}, 208 | Class = _currentClass 209 | }, 210 | new Method("__call__", classType.TypeParameters[1]) 211 | { 212 | Summary = "Fires the event.", 213 | Parameters = 214 | { 215 | new Parameter("*args", PythonType.Any), 216 | new Parameter("**kwargs", PythonType.Any) 217 | }, 218 | Class = _currentClass 219 | } 220 | } 221 | }); 222 | } 223 | 224 | private bool IsStaticReadonly(BasePropertyDeclarationSyntax node) 225 | { 226 | if (!(node is PropertyDeclarationSyntax propertyNode) || 227 | !HasModifier(propertyNode, "static")) 228 | { 229 | return false; 230 | } 231 | 232 | if (propertyNode.ExpressionBody != null) 233 | { 234 | return true; 235 | } 236 | 237 | var accessors = propertyNode.AccessorList?.Accessors; 238 | return accessors.HasValue && !accessors.Value.Any(x => x.Kind() == SyntaxKind.SetAccessorDeclaration); 239 | } 240 | } 241 | } 242 | 243 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/csharp,python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=csharp,python 3 | 4 | ### Csharp ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | ## 8 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 9 | 10 | # User-specific files 11 | *.rsuser 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # User-specific files (MonoDevelop/Xamarin Studio) 18 | *.userprefs 19 | 20 | # Mono auto generated files 21 | mono_crash.* 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | x64/ 29 | x86/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*[.json, .xml, .info] 146 | 147 | # Visual Studio code coverage results 148 | *.coverage 149 | *.coveragexml 150 | 151 | # NCrunch 152 | _NCrunch_* 153 | .*crunch*.local.xml 154 | nCrunchTemp_* 155 | 156 | # MightyMoose 157 | *.mm.* 158 | AutoTest.Net/ 159 | 160 | # Web workbench (sass) 161 | .sass-cache/ 162 | 163 | # Installshield output folder 164 | [Ee]xpress/ 165 | 166 | # DocProject is a documentation generator add-in 167 | DocProject/buildhelp/ 168 | DocProject/Help/*.HxT 169 | DocProject/Help/*.HxC 170 | DocProject/Help/*.hhc 171 | DocProject/Help/*.hhk 172 | DocProject/Help/*.hhp 173 | DocProject/Help/Html2 174 | DocProject/Help/html 175 | 176 | # Click-Once directory 177 | publish/ 178 | 179 | # Publish Web Output 180 | *.[Pp]ublish.xml 181 | *.azurePubxml 182 | # Note: Comment the next line if you want to checkin your web deploy settings, 183 | # but database connection strings (with potential passwords) will be unencrypted 184 | *.pubxml 185 | *.publishproj 186 | 187 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 188 | # checkin your Azure Web App publish settings, but sensitive information contained 189 | # in these scripts will be unencrypted 190 | PublishScripts/ 191 | 192 | # NuGet Packages 193 | *.nupkg 194 | # NuGet Symbol Packages 195 | *.snupkg 196 | # The packages folder can be ignored because of Package Restore 197 | **/[Pp]ackages/* 198 | # except build/, which is used as an MSBuild target. 199 | !**/[Pp]ackages/build/ 200 | # Uncomment if necessary however generally it will be regenerated when needed 201 | #!**/[Pp]ackages/repositories.config 202 | # NuGet v3's project.json files produces more ignorable files 203 | *.nuget.props 204 | *.nuget.targets 205 | 206 | # Microsoft Azure Build Output 207 | csx/ 208 | *.build.csdef 209 | 210 | # Microsoft Azure Emulator 211 | ecf/ 212 | rcf/ 213 | 214 | # Windows Store app package directories and files 215 | AppPackages/ 216 | BundleArtifacts/ 217 | Package.StoreAssociation.xml 218 | _pkginfo.txt 219 | *.appx 220 | *.appxbundle 221 | *.appxupload 222 | 223 | # Visual Studio cache files 224 | # files ending in .cache can be ignored 225 | *.[Cc]ache 226 | # but keep track of directories ending in .cache 227 | !?*.[Cc]ache/ 228 | 229 | # Others 230 | ClientBin/ 231 | ~$* 232 | *~ 233 | *.dbmdl 234 | *.dbproj.schemaview 235 | *.jfm 236 | *.pfx 237 | *.publishsettings 238 | orleans.codegen.cs 239 | 240 | # Including strong name files can present a security risk 241 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 242 | #*.snk 243 | 244 | # Since there are multiple workflows, uncomment next line to ignore bower_components 245 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 246 | #bower_components/ 247 | 248 | # RIA/Silverlight projects 249 | Generated_Code/ 250 | 251 | # Backup & report files from converting an old project file 252 | # to a newer Visual Studio version. Backup files are not needed, 253 | # because we have git ;-) 254 | _UpgradeReport_Files/ 255 | Backup*/ 256 | UpgradeLog*.XML 257 | UpgradeLog*.htm 258 | ServiceFabricBackup/ 259 | *.rptproj.bak 260 | 261 | # SQL Server files 262 | *.mdf 263 | *.ldf 264 | *.ndf 265 | 266 | # Business Intelligence projects 267 | *.rdl.data 268 | *.bim.layout 269 | *.bim_*.settings 270 | *.rptproj.rsuser 271 | *- [Bb]ackup.rdl 272 | *- [Bb]ackup ([0-9]).rdl 273 | *- [Bb]ackup ([0-9][0-9]).rdl 274 | 275 | # Microsoft Fakes 276 | FakesAssemblies/ 277 | 278 | # GhostDoc plugin setting file 279 | *.GhostDoc.xml 280 | 281 | # Node.js Tools for Visual Studio 282 | .ntvs_analysis.dat 283 | node_modules/ 284 | 285 | # Visual Studio 6 build log 286 | *.plg 287 | 288 | # Visual Studio 6 workspace options file 289 | *.opt 290 | 291 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 292 | *.vbw 293 | 294 | # Visual Studio LightSwitch build output 295 | **/*.HTMLClient/GeneratedArtifacts 296 | **/*.DesktopClient/GeneratedArtifacts 297 | **/*.DesktopClient/ModelManifest.xml 298 | **/*.Server/GeneratedArtifacts 299 | **/*.Server/ModelManifest.xml 300 | _Pvt_Extensions 301 | 302 | # Paket dependency manager 303 | .paket/paket.exe 304 | paket-files/ 305 | 306 | # FAKE - F# Make 307 | .fake/ 308 | 309 | # CodeRush personal settings 310 | .cr/personal 311 | 312 | # Python Tools for Visual Studio (PTVS) 313 | __pycache__/ 314 | *.pyc 315 | 316 | # Cake - Uncomment if you are using it 317 | # tools/** 318 | # !tools/packages.config 319 | 320 | # Tabs Studio 321 | *.tss 322 | 323 | # Telerik's JustMock configuration file 324 | *.jmconfig 325 | 326 | # BizTalk build output 327 | *.btp.cs 328 | *.btm.cs 329 | *.odx.cs 330 | *.xsd.cs 331 | 332 | # OpenCover UI analysis results 333 | OpenCover/ 334 | 335 | # Azure Stream Analytics local run output 336 | ASALocalRun/ 337 | 338 | # MSBuild Binary and Structured Log 339 | *.binlog 340 | 341 | # NVidia Nsight GPU debugger configuration file 342 | *.nvuser 343 | 344 | # MFractors (Xamarin productivity tool) working folder 345 | .mfractor/ 346 | 347 | # Local History for Visual Studio 348 | .localhistory/ 349 | 350 | # BeatPulse healthcheck temp database 351 | healthchecksdb 352 | 353 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 354 | MigrationBackup/ 355 | 356 | # Ionide (cross platform F# VS Code tools) working folder 357 | .ionide/ 358 | 359 | ### Python ### 360 | # Byte-compiled / optimized / DLL files 361 | *.py[cod] 362 | *$py.class 363 | 364 | # C extensions 365 | *.so 366 | 367 | # Distribution / packaging 368 | .Python 369 | build/ 370 | develop-eggs/ 371 | dist/ 372 | downloads/ 373 | eggs/ 374 | .eggs/ 375 | lib/ 376 | lib64/ 377 | parts/ 378 | sdist/ 379 | var/ 380 | wheels/ 381 | pip-wheel-metadata/ 382 | share/python-wheels/ 383 | *.egg-info/ 384 | .installed.cfg 385 | *.egg 386 | MANIFEST 387 | 388 | # PyInstaller 389 | # Usually these files are written by a python script from a template 390 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 391 | *.manifest 392 | *.spec 393 | 394 | # Installer logs 395 | pip-log.txt 396 | pip-delete-this-directory.txt 397 | 398 | # Unit test / coverage reports 399 | htmlcov/ 400 | .tox/ 401 | .nox/ 402 | .coverage 403 | .coverage.* 404 | .cache 405 | nosetests.xml 406 | coverage.xml 407 | *.cover 408 | *.py,cover 409 | .hypothesis/ 410 | .pytest_cache/ 411 | pytestdebug.log 412 | 413 | # Translations 414 | *.mo 415 | *.pot 416 | 417 | # Django stuff: 418 | local_settings.py 419 | db.sqlite3 420 | db.sqlite3-journal 421 | 422 | # Flask stuff: 423 | instance/ 424 | .webassets-cache 425 | 426 | # Scrapy stuff: 427 | .scrapy 428 | 429 | # Sphinx documentation 430 | docs/_build/ 431 | doc/_build/ 432 | 433 | # PyBuilder 434 | target/ 435 | 436 | # Jupyter Notebook 437 | .ipynb_checkpoints 438 | 439 | # IPython 440 | profile_default/ 441 | ipython_config.py 442 | 443 | # pyenv 444 | .python-version 445 | 446 | # pipenv 447 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 448 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 449 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 450 | # install all needed dependencies. 451 | #Pipfile.lock 452 | 453 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 454 | __pypackages__/ 455 | 456 | # Celery stuff 457 | celerybeat-schedule 458 | celerybeat.pid 459 | 460 | # SageMath parsed files 461 | *.sage.py 462 | 463 | # Environments 464 | .env 465 | .venv 466 | env/ 467 | venv/ 468 | ENV/ 469 | env.bak/ 470 | venv.bak/ 471 | pythonenv* 472 | 473 | # Spyder project settings 474 | .spyderproject 475 | .spyproject 476 | 477 | # Rope project settings 478 | .ropeproject 479 | 480 | # mkdocs documentation 481 | /site 482 | 483 | # mypy 484 | .mypy_cache/ 485 | .dmypy.json 486 | dmypy.json 487 | 488 | # Pyre type checker 489 | .pyre/ 490 | 491 | # pytype static type analyzer 492 | .pytype/ 493 | 494 | # profiling data 495 | .prof 496 | 497 | # End of https://www.toptal.com/developers/gitignore/api/csharp,python 498 | 499 | # Integration test output 500 | generated 501 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2020 QuantConnect Corporation 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Parser/TypeConverter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using Microsoft.CodeAnalysis; 17 | using System.Collections.Generic; 18 | using QuantConnectStubsGenerator.Model; 19 | 20 | namespace QuantConnectStubsGenerator.Parser 21 | { 22 | /// 23 | /// The TypeConverter is responsible for converting AST nodes into PythonType instances. 24 | /// 25 | public class TypeConverter 26 | { 27 | private readonly SemanticModel _model; 28 | 29 | public TypeConverter(SemanticModel model) 30 | { 31 | _model = model; 32 | } 33 | 34 | /// 35 | /// Returns the symbol of the given node. 36 | /// Returns null if the semantic model does not contain a symbol for the node. 37 | /// 38 | public ISymbol GetSymbol(SyntaxNode node) 39 | { 40 | // ReSharper disable once ConstantNullCoalescingCondition 41 | return _model.GetDeclaredSymbol(node) ?? _model.GetSymbolInfo(node).Symbol; 42 | } 43 | 44 | /// 45 | /// Returns the Python type of the given node. 46 | /// Returns an aliased typing.Any if there is no Python type for the given symbol. 47 | /// 48 | public PythonType GetType(SyntaxNode node, bool skipPythonTypeCheck = false, bool skipTypeNormalization = false, bool isParameter = false) 49 | { 50 | var symbol = GetSymbol(node); 51 | 52 | if (symbol == null) 53 | { 54 | return TryConvertPythonnetType(node.ToFullString().Trim(), out var pythonType) 55 | ? pythonType 56 | : PythonType.Any; 57 | } 58 | 59 | return GetType(symbol, skipPythonTypeCheck, skipTypeNormalization, isParameter); 60 | } 61 | 62 | /// 63 | /// Returns the Python type of the given symbol. 64 | /// Returns an aliased typing.Any if there is no Python type for the given symbol. 65 | /// 66 | public PythonType GetType(ISymbol symbol, bool skipPythonTypeCheck = false, bool skipTypeNormalization = false, bool isParameter = false) 67 | { 68 | // Handle arrays 69 | if (symbol is IArrayTypeSymbol arrayTypeSymbol) 70 | { 71 | var listType = new PythonType("List", "typing"); 72 | listType.TypeParameters.Add(GetType(arrayTypeSymbol.ElementType, isParameter: isParameter)); 73 | return listType; 74 | } 75 | 76 | // Use typing.Any as fallback if there is no type information in the given symbol 77 | if (symbol == null || symbol.Name == "" || symbol.ContainingNamespace == null) 78 | { 79 | return PythonType.Any; 80 | } 81 | 82 | if (TryConvertPythonnetType(symbol.Name, out var pythonType)) 83 | { 84 | return pythonType; 85 | } 86 | 87 | var name = GetTypeName(symbol); 88 | var ns = symbol.ContainingNamespace.ToDisplayString(); 89 | 90 | var type = new PythonType(name, ns); 91 | 92 | // Process type parameters 93 | if (symbol is ITypeParameterSymbol) 94 | { 95 | type.IsNamedTypeParameter = true; 96 | } 97 | 98 | // Process named type parameters 99 | if (symbol is INamedTypeSymbol namedTypeSymbol) 100 | { 101 | // Process delegates 102 | if (namedTypeSymbol.DelegateInvokeMethod != null) 103 | { 104 | var parameters = new List(); 105 | 106 | foreach (var parameter in namedTypeSymbol.DelegateInvokeMethod.Parameters) 107 | { 108 | parameters.Add(GetType(parameter.Type, isParameter: isParameter)); 109 | } 110 | 111 | var returnType = GetType(namedTypeSymbol.DelegateInvokeMethod.ReturnType, isParameter: isParameter); 112 | if (returnType.Equals(PythonType.None)) 113 | { 114 | returnType = PythonType.Any; 115 | } 116 | parameters.Add(returnType); 117 | 118 | return new PythonType("Callable", "typing") 119 | { 120 | TypeParameters = parameters 121 | }; 122 | } 123 | 124 | foreach (var typeParameter in namedTypeSymbol.TypeArguments) 125 | { 126 | var paramType = GetType(typeParameter, isParameter: isParameter); 127 | 128 | if (typeParameter is ITypeParameterSymbol) 129 | { 130 | paramType.IsNamedTypeParameter = true; 131 | } 132 | 133 | type.TypeParameters.Add(paramType); 134 | } 135 | } 136 | 137 | var result = CSharpTypeToPythonType(type, skipPythonTypeCheck); 138 | if (!skipTypeNormalization) 139 | { 140 | result = NormalizeType(result, isParameter); 141 | } 142 | return result; 143 | } 144 | 145 | private string GetTypeName(ISymbol symbol) 146 | { 147 | var nameParts = new List(); 148 | 149 | var currentSymbol = symbol; 150 | while (currentSymbol != null) 151 | { 152 | nameParts.Add(currentSymbol.Name); 153 | currentSymbol = currentSymbol.ContainingType; 154 | } 155 | 156 | nameParts.Reverse(); 157 | 158 | if (symbol is ITypeParameterSymbol typeParameterSymbol) 159 | { 160 | if (typeParameterSymbol.DeclaringMethod != null) 161 | { 162 | nameParts.Insert(1, typeParameterSymbol.DeclaringMethod.Name); 163 | } 164 | } 165 | 166 | return string.Join(".", nameParts); 167 | } 168 | 169 | /// 170 | /// Converts a C# type to a Python type. 171 | /// This method handles conversions like the one from System.String to str. 172 | /// If the Type object doesn't need to be converted, the originally provided type is returned. 173 | /// 174 | private PythonType CSharpTypeToPythonType(PythonType type, bool skipPythonTypeCheck = false) 175 | { 176 | if (type.Namespace == "System" && !skipPythonTypeCheck) 177 | { 178 | switch (type.Name) 179 | { 180 | case "Char": 181 | case "String": 182 | return new PythonType("str"); 183 | case "Byte": 184 | case "SByte": 185 | case "Int16": 186 | case "Int32": 187 | case "Int64": 188 | case "UInt16": 189 | case "UInt32": 190 | case "UInt64": 191 | return new PythonType("int"); 192 | case "Single": 193 | case "Double": 194 | case "Decimal": 195 | return new PythonType("float"); 196 | case "Boolean": 197 | return new PythonType("bool"); 198 | case "Void": 199 | return new PythonType("None"); 200 | case "DateTime": 201 | return new PythonType("datetime", "datetime"); 202 | case "TimeSpan": 203 | return new PythonType("timedelta", "datetime"); 204 | case "Nullable": 205 | type.Name = "Optional"; 206 | type.Namespace = "typing"; 207 | break; 208 | case "Type": 209 | type.Name = "Type"; 210 | type.Namespace = "typing"; 211 | break; 212 | } 213 | } 214 | 215 | // C# types that don't have a Python-equivalent or that we don't parse are converted to an aliased Any 216 | if (type.Namespace == "") 217 | { 218 | return new PythonType("Any", "typing") 219 | { 220 | Alias = type.Name.Replace('.', '_') 221 | }; 222 | } 223 | 224 | return type; 225 | } 226 | 227 | private static bool TryConvertPythonnetType(string typeName, out PythonType pythonType) 228 | { 229 | switch (typeName) 230 | { 231 | case "PyObject": 232 | pythonType = PythonType.Any; 233 | return true; 234 | 235 | case "PyList": 236 | pythonType = new PythonType("List", "typing") 237 | { 238 | TypeParameters = new List { PythonType.Any } 239 | }; 240 | return true; 241 | 242 | case "PyDict": 243 | pythonType = new PythonType("Dict", "typing") 244 | { 245 | TypeParameters = new List 246 | { 247 | PythonType.Any, PythonType.Any 248 | } 249 | }; 250 | return true; 251 | 252 | default: 253 | pythonType = null; 254 | return false; 255 | } 256 | } 257 | 258 | private static PythonType NormalizeType(PythonType type, bool isParameter) 259 | { 260 | var isList = false; 261 | var isGeneric = false; 262 | if (type.Namespace == "System.Collections.Generic" && type.TypeParameters.Count == 1) 263 | { 264 | isGeneric = true; 265 | 266 | if (type.Name == "IReadOnlyList" || type.Name == "IReadOnlyCollection") 267 | { 268 | return new PythonType("Sequence", "typing") 269 | { 270 | TypeParameters = { NormalizeType(type.TypeParameters[0], isParameter) } 271 | }; 272 | } 273 | else if (type.Name == "IList" || type.Name == "List") 274 | { 275 | isList = true; 276 | } 277 | else if (type.Name == "IEnumerable") 278 | { 279 | if (!isParameter) 280 | { 281 | return new PythonType("Iterable", "typing") 282 | { 283 | TypeParameters = { NormalizeType(type.TypeParameters[0], isParameter) } 284 | }; 285 | } 286 | isList = true; 287 | } 288 | } 289 | else if (type.Namespace == "System.Collections" && type.Name == "IList") 290 | { 291 | isList = true; 292 | } 293 | 294 | if (isList) 295 | { 296 | if (isGeneric) 297 | { 298 | return new PythonType("List", "typing") 299 | { 300 | TypeParameters = { NormalizeType(type.TypeParameters[0], isParameter) } 301 | }; 302 | } 303 | 304 | return new PythonType("List", "typing") 305 | { 306 | TypeParameters = { PythonType.Any } 307 | }; 308 | } 309 | 310 | return type; 311 | } 312 | } 313 | } 314 | 315 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator/Parser/BaseParser.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using System.Linq; 17 | using System.Text.RegularExpressions; 18 | using System.Xml; 19 | using Microsoft.CodeAnalysis; 20 | using Microsoft.CodeAnalysis.CSharp; 21 | using Microsoft.CodeAnalysis.CSharp.Syntax; 22 | using QuantConnectStubsGenerator.Model; 23 | 24 | namespace QuantConnectStubsGenerator.Parser 25 | { 26 | public abstract class BaseParser : CSharpSyntaxWalker 27 | { 28 | protected readonly ParseContext _context; 29 | protected readonly SemanticModel _model; 30 | 31 | protected readonly TypeConverter _typeConverter; 32 | 33 | protected Namespace _currentNamespace; 34 | protected Class _currentClass; 35 | 36 | protected BaseParser(ParseContext context, SemanticModel model) 37 | { 38 | _context = context; 39 | _model = model; 40 | 41 | _typeConverter = new TypeConverter(model); 42 | } 43 | 44 | /// 45 | /// Handles 'namespace QuantConnect.Indicators;' sintax 46 | /// 47 | public override void VisitFileScopedNamespaceDeclaration(FileScopedNamespaceDeclarationSyntax node) 48 | { 49 | var name = node.Name.ToString(); 50 | SetCurrentNamespace(name); 51 | base.VisitFileScopedNamespaceDeclaration(node); 52 | } 53 | 54 | /// 55 | /// Handles 'namespace QuantConnect.Indicators { }' sintax 56 | /// 57 | public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) 58 | { 59 | var name = node.Name.ToString(); 60 | SetCurrentNamespace(name); 61 | base.VisitNamespaceDeclaration(node); 62 | } 63 | 64 | public override void VisitClassDeclaration(ClassDeclarationSyntax node) 65 | { 66 | if (ShouldSkip(node)) 67 | { 68 | return; 69 | } 70 | 71 | EnterClass(node); 72 | base.VisitClassDeclaration(node); 73 | ExitClass(); 74 | } 75 | 76 | public override void VisitStructDeclaration(StructDeclarationSyntax node) 77 | { 78 | if (ShouldSkip(node)) 79 | { 80 | return; 81 | } 82 | 83 | EnterClass(node); 84 | base.VisitStructDeclaration(node); 85 | ExitClass(); 86 | } 87 | 88 | public override void VisitEnumDeclaration(EnumDeclarationSyntax node) 89 | { 90 | if (ShouldSkip(node)) 91 | { 92 | return; 93 | } 94 | 95 | EnterClass(node); 96 | base.VisitEnumDeclaration(node); 97 | ExitClass(); 98 | } 99 | 100 | public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) 101 | { 102 | if (ShouldSkip(node)) 103 | { 104 | return; 105 | } 106 | 107 | EnterClass(node); 108 | base.VisitInterfaceDeclaration(node); 109 | ExitClass(); 110 | } 111 | 112 | /// 113 | /// EnterClass is the method that is called whenever a class/struct/enum/interface is entered. 114 | /// In the BaseParser it is assumed that the class that is entered is already registered in the namespace. 115 | /// In the ClassParser, which runs before any other parsers, this method is overridden to register classes. 116 | /// 117 | protected virtual void EnterClass(BaseTypeDeclarationSyntax node) 118 | { 119 | _currentClass = _currentNamespace.GetClassByType(_typeConverter.GetType(node, true, true, false)); 120 | } 121 | 122 | private void ExitClass() 123 | { 124 | _currentClass = _currentClass?.ParentClass; 125 | } 126 | 127 | /// 128 | /// Check if a node has a modifier like private or static. 129 | /// 130 | protected bool HasModifier(MemberDeclarationSyntax node, string modifier) 131 | { 132 | return HasModifier(node.Modifiers, modifier); 133 | } 134 | 135 | /// 136 | /// Check if a node has a modifier like private or static. 137 | /// 138 | protected bool HasModifier(SyntaxTokenList modifiers, string modifier) 139 | { 140 | return modifiers.Any(m => m.Text == modifier); 141 | } 142 | 143 | /// 144 | /// Check if a node has an attribute like Obsolete or StubsIgnore. 145 | /// 146 | protected bool HasAttribute(SyntaxList attributeList, string attribute) 147 | { 148 | return attributeList.Any(list => list.Attributes.Any(x => x.Name.ToString() == attribute)); 149 | } 150 | 151 | /// 152 | /// We skip internal or private nodes 153 | /// 154 | protected bool ShouldSkip(MemberDeclarationSyntax node) 155 | { 156 | if (HasAttribute(node.AttributeLists, "StubsIgnore")) 157 | { 158 | return true; 159 | } 160 | 161 | if (HasModifier(node, "private") || HasModifier(node, "internal")) 162 | { 163 | return true; 164 | } 165 | 166 | if (node.Modifiers.Count() == 0 && node.Parent != null && node.Parent.IsKind(SyntaxKind.InterfaceDeclaration)) 167 | { 168 | // interfaces properties/methods are public by default, so they depend on the parent really 169 | if (node.Parent is InterfaceDeclarationSyntax interfaceDeclarationSyntax) 170 | { 171 | var modifiers = interfaceDeclarationSyntax.Modifiers; 172 | return !HasModifier(modifiers, "public") && !HasModifier(modifiers, "protected"); 173 | } 174 | return true; 175 | } 176 | // some classes don't any access modifier set, which means private 177 | return !HasModifier(node, "public") && !HasModifier(node, "protected"); 178 | } 179 | 180 | /// 181 | /// Returns the deprecation message if the node is marked obsolete, or null if it is not. 182 | /// 183 | protected string GetDeprecationReason(MemberDeclarationSyntax node) 184 | { 185 | foreach (var attributeList in node.AttributeLists) 186 | { 187 | foreach (var attribute in attributeList.Attributes) 188 | { 189 | if (attribute.Name.ToString() == "Obsolete") 190 | { 191 | if (attribute.ArgumentList == null) 192 | { 193 | return "This member is marked as obsolete."; 194 | } 195 | 196 | var arguments = attribute.ArgumentList.Arguments; 197 | if (arguments.Count == 0) 198 | { 199 | return "This member is marked as obsolete."; 200 | } 201 | 202 | var reason = _model.GetConstantValue(arguments[0].Expression); 203 | var reasonMessage = reason.HasValue ? reason.Value as string : arguments[0].Expression.ToString(); 204 | 205 | // The stubs are meant to make writing algorithms easier 206 | // If a member is not deprecated for algorithm use, we don't mark it as deprecated at all 207 | if (!reasonMessage.Contains("provided for algorithm use only")) 208 | { 209 | return reasonMessage; 210 | } 211 | } 212 | } 213 | } 214 | 215 | return null; 216 | } 217 | 218 | /// 219 | /// Parses the documentation above a node to an XML element. 220 | /// If the documentation contains a summary, this is then accessible with element["summary"]. 221 | /// 222 | protected XmlDocument ParseDocumentation(SyntaxNode node) 223 | { 224 | var lines = node 225 | .GetLeadingTrivia() 226 | .ToString() 227 | .Trim() 228 | .Split("\n") 229 | .Select(line => line.Trim()) 230 | .ToList(); 231 | 232 | // LeadingTrivia of a node contains all comments above it 233 | // We skip everything before the last uncommented line to get only the XML directly above the node 234 | var skips = 0; 235 | for (var i = 0; i < lines.Count; i++) 236 | { 237 | if (lines[i] == "") 238 | { 239 | skips = i; 240 | } 241 | } 242 | 243 | var xmlLines = lines 244 | .Skip(skips) 245 | .Select(line => 246 | { 247 | if (line.StartsWith("/// ")) 248 | { 249 | return line.Substring(4); 250 | } 251 | 252 | if (line.StartsWith("///")) 253 | { 254 | return line.Substring(3); 255 | } 256 | 257 | return line; 258 | }); 259 | 260 | var xml = string.Join("\n", xmlLines).Replace("&", "&").Trim(); 261 | 262 | if (!xml.StartsWith("<")) 263 | { 264 | xml = ""; 265 | } 266 | 267 | var doc = new XmlDocument(); 268 | 269 | try 270 | { 271 | doc.LoadXml($"{xml}"); 272 | } 273 | catch 274 | { 275 | doc.LoadXml(""); 276 | } 277 | 278 | return doc; 279 | } 280 | 281 | /// 282 | /// Appends the given text to the given summary. 283 | /// An empty line is placed between the current summary and the given text. 284 | /// 285 | protected string AppendSummary(string currentSummary, string text) 286 | { 287 | return currentSummary != null ? currentSummary + "\n\n" + text : text; 288 | } 289 | 290 | protected XmlDocument GetXmlDocumentation(MemberDeclarationSyntax node, CodeEntityType codeEntityType) 291 | { 292 | var doc = ParseDocumentation(node); 293 | var xmlSummary = doc["root"]["summary"]; 294 | 295 | if (HasModifier(node, "protected")) 296 | { 297 | var hasSummary = xmlSummary != null; 298 | xmlSummary ??= doc.CreateElement("summary"); 299 | doc["root"].AppendChild(xmlSummary); 300 | xmlSummary.AppendChild(doc.CreateTextNode((!hasSummary ? "" : "\n\n") + $"This {nameof(codeEntityType)} is protected.")); 301 | } 302 | 303 | var deprecationReason = GetDeprecationReason(node); 304 | if (deprecationReason != null) 305 | { 306 | var hasSummary = xmlSummary != null; 307 | xmlSummary ??= doc.CreateElement("summary"); 308 | doc["root"].AppendChild(xmlSummary); 309 | xmlSummary.AppendChild(doc.CreateTextNode((!hasSummary ? "" : "\n\n") + deprecationReason)); 310 | } 311 | 312 | return doc; 313 | } 314 | 315 | /// 316 | /// Format a default C# value into a default Python value. 317 | /// 318 | protected string FormatValue(string value) 319 | { 320 | // null to None 321 | if (value == "null") 322 | { 323 | return "None"; 324 | } 325 | 326 | // Boolean true 327 | if (value == "true") 328 | { 329 | return "True"; 330 | } 331 | 332 | // Boolean false 333 | if (value == "false") 334 | { 335 | return "False"; 336 | } 337 | 338 | // Numbers 339 | if (Regex.IsMatch(value, "^-?[0-9.]+m?$")) 340 | { 341 | // If the value is a number, remove a potential suffix like "m" in 1.0m 342 | if (value.EndsWith("m")) 343 | { 344 | return value.Substring(0, value.Length - 1); 345 | } 346 | 347 | return value; 348 | } 349 | 350 | // Strings 351 | if (Regex.IsMatch(value, "^@?\"[^\"]+\"$")) 352 | { 353 | if (value.StartsWith("@")) 354 | { 355 | value = value.Substring(1); 356 | } 357 | 358 | // Escape backslashes 359 | value = value.Replace("\\", "\\\\"); 360 | 361 | return value; 362 | } 363 | 364 | return "..."; 365 | } 366 | 367 | private void SetCurrentNamespace(string name) 368 | { 369 | if (!_context.HasNamespace(name)) 370 | { 371 | _context.RegisterNamespace(new Namespace(name)); 372 | } 373 | 374 | _currentNamespace = _context.GetNamespaceByName(name); 375 | } 376 | } 377 | } 378 | 379 | -------------------------------------------------------------------------------- /QuantConnectStubsGenerator.Tests/GeneratorTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. 3 | * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | using Microsoft.CodeAnalysis; 17 | using Microsoft.CodeAnalysis.CSharp; 18 | using NUnit.Framework; 19 | using QuantConnectStubsGenerator.Model; 20 | using System.Collections.Generic; 21 | using System.Linq; 22 | 23 | namespace QuantConnectStubsGenerator.Tests 24 | { 25 | [TestFixture] 26 | public class GeneratorTests 27 | { 28 | [TestCase("public", true)] 29 | [TestCase("protected", true)] 30 | [TestCase("", false)] 31 | [TestCase("private", false)] 32 | public void Interfaces(string interfaceModifier, bool expected) 33 | { 34 | var testGenerator = new TestGenerator 35 | { 36 | Files = new() 37 | { 38 | { "Test.cs", $@" 39 | using System; 40 | 41 | namespace QuantConnect.Benchmarks 42 | {{ 43 | /// 44 | /// Specifies how to compute a benchmark for an algorithm 45 | /// 46 | {interfaceModifier} interface IBenchmark 47 | {{ 48 | /// 49 | /// Evaluates this benchmark at the specified time 50 | /// 51 | /// The time to evaluate the benchmark at 52 | /// The value of the benchmark at the specified time 53 | decimal Evaluate(DateTime time); 54 | 55 | DateTime TestProperty {{get;}} 56 | }} 57 | }}" } 58 | } 59 | }; 60 | 61 | var result = testGenerator.GenerateModelsPublic(); 62 | 63 | var namespaces = result.GetNamespaces().ToList(); 64 | Assert.AreEqual(2, namespaces.Count); 65 | 66 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 67 | var benchmarksNameSpace = namespaces.Single(x => x.Name == "QuantConnect.Benchmarks"); 68 | 69 | if (!expected) 70 | { 71 | Assert.AreEqual(0, benchmarksNameSpace.GetClasses().Count()); 72 | return; 73 | } 74 | var benchmark = benchmarksNameSpace.GetClasses().Single(); 75 | 76 | Assert.AreEqual("Evaluate", benchmark.Methods.Single().Name); 77 | Assert.IsFalse(string.IsNullOrEmpty(benchmark.Methods.Single().Summary)); 78 | 79 | Assert.AreEqual("TestProperty", benchmark.Properties.Single().Name); 80 | Assert.IsTrue(string.IsNullOrEmpty(benchmark.Properties.Single().Summary)); 81 | } 82 | 83 | [TestCase("ConstProperty1", true)] 84 | [TestCase("ConstProperty2", true)] 85 | [TestCase("NonConstProperty1", false)] 86 | [TestCase("NonConstProperty2", false)] 87 | [TestCase("NonConstProperty3", false)] 88 | [TestCase("NonConstProperty4", false)] 89 | [TestCase("NonConstProperty5", false)] 90 | [TestCase("NonConstProperty6", false)] 91 | [TestCase("NonConstProperty7", false)] 92 | [TestCase("NonConstProperty8", false)] 93 | [TestCase("NonConstProperty9", false)] 94 | [TestCase("NonConstProperty10", false)] 95 | [TestCase("NonConstProperty11", false)] 96 | [TestCase("NonConstProperty12", false)] 97 | public void ConstantProperties(string propertyName, bool shouldBeConstant) 98 | { 99 | var testGenerator = new TestGenerator 100 | { 101 | Files = new() 102 | { 103 | { "Test.cs", $@" 104 | using System; 105 | 106 | namespace QuantConnect.Properties 107 | {{ 108 | /// 109 | /// Specifies how to compute a benchmark for an algorithm 110 | /// 111 | public class PropertiesClass 112 | {{ 113 | public static int ConstProperty1 => 1; 114 | 115 | public static int ConstProperty2 {{ get; }} 116 | 117 | public int NonConstProperty1 {{ get; set; }} 118 | 119 | public int NonConstProperty2 {{ get; protected set; }} 120 | 121 | public int NonConstProperty3 {{ get; private set; }} 122 | 123 | public int NonConstProperty4 {{ get; internal set; }} 124 | 125 | public int NonConstProperty5 {{ get; protected internal set; }} 126 | 127 | public static int NonConstProperty6 {{ get; private protected set; }} 128 | 129 | public static int NonConstProperty7 {{ get; set; }} 130 | 131 | public static int NonConstProperty8 {{ get; protected set; }} 132 | 133 | public static int NonConstProperty9 {{ get; private set; }} 134 | 135 | public static int NonConstProperty10 {{ get; internal set; }} 136 | 137 | public static int NonConstProperty11 {{ get; protected internal set; }} 138 | 139 | public int NonConstProperty12 => 1; 140 | }} 141 | }}" } 142 | } 143 | }; 144 | 145 | var result = testGenerator.GenerateModelsPublic(); 146 | 147 | var namespaces = result.GetNamespaces().ToList(); 148 | Assert.AreEqual(2, namespaces.Count); 149 | 150 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 151 | var propertiesNameSpace = namespaces.Single(x => x.Name == "QuantConnect.Properties"); 152 | 153 | var propertiesClass = propertiesNameSpace.GetClasses().Single(); 154 | 155 | Assert.AreEqual(14, propertiesClass.Properties.Count); 156 | 157 | var property = propertiesClass.Properties.Single(x => x.Name == propertyName); 158 | Assert.AreEqual(shouldBeConstant, property.Constant); 159 | } 160 | 161 | [Test] 162 | public void GenericMethods() 163 | { 164 | var testGenerator = new TestGenerator 165 | { 166 | Files = new() 167 | { 168 | { "Test.cs", @" 169 | namespace QuantConnect.GenericMethodsTest 170 | { 171 | public class TestClass 172 | { 173 | public int History(string parameter) 174 | { 175 | return 1; 176 | } 177 | public T History(T parameter) 178 | { 179 | return parameter; 180 | } 181 | public PyObject History(int parameter) 182 | { 183 | return null; 184 | } 185 | } 186 | }" } 187 | } 188 | }; 189 | 190 | var result = testGenerator.GenerateModelsPublic(); 191 | 192 | var namespaces = result.GetNamespaces().ToList(); 193 | Assert.AreEqual(2, namespaces.Count); 194 | 195 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 196 | var testNameSpace = namespaces.Single(x => x.Name == "QuantConnect.GenericMethodsTest"); 197 | 198 | var testClass = testNameSpace.GetClasses().Single(); 199 | Assert.AreEqual("TestClass", testClass.Type.Name); 200 | 201 | var testMethodCount = testClass.Methods.Count; 202 | Assert.AreEqual(0, testMethodCount); 203 | 204 | var innerClass = testClass.InnerClasses.Single(); 205 | 206 | Assert.AreEqual(0, innerClass.Type.TypeParameters?.Count ?? 0); 207 | Assert.IsNotNull(innerClass.Methods.SingleOrDefault(x => x.Name == "__getitem__")); 208 | Assert.IsNotNull(innerClass.Methods.SingleOrDefault(x => x.Name == "__call__")); 209 | 210 | var innerInnerClass = innerClass.InnerClasses.Single(); 211 | 212 | Assert.AreNotEqual(0, innerInnerClass.Type.TypeParameters?.Count ?? 0); 213 | Assert.AreEqual(2, innerInnerClass.Methods.Count(x => x.Name == "__call__")); 214 | } 215 | 216 | [Test] 217 | public void OutParameters() 218 | { 219 | var testGenerator = new TestGenerator 220 | { 221 | Files = new() 222 | { 223 | { "Test.cs", @" 224 | namespace QuantConnect.OutParametersTest 225 | { 226 | public class TestClass 227 | { 228 | public int TestMethod(int parameter, out string outParameter) 229 | { 230 | outParameter = parameter.ToString(); 231 | return parameter; 232 | } 233 | } 234 | }" } 235 | } 236 | }; 237 | 238 | var result = testGenerator.GenerateModelsPublic(); 239 | 240 | var namespaces = result.GetNamespaces().ToList(); 241 | Assert.AreEqual(2, namespaces.Count); 242 | 243 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 244 | var testNameSpace = namespaces.Single(x => x.Name == "QuantConnect.OutParametersTest"); 245 | 246 | var testClass = testNameSpace.GetClasses().Single(); 247 | Assert.AreEqual("TestClass", testClass.Type.Name); 248 | 249 | var testMethod = testClass.Methods.Single(); 250 | Assert.AreEqual("TestMethod", testMethod.Name); 251 | Assert.AreEqual(2, testMethod.Parameters.Count); 252 | 253 | // First parameter 254 | var parameter1 = testMethod.Parameters[0]; 255 | Assert.AreEqual("parameter", parameter1.Name); 256 | Assert.AreEqual("int", parameter1.Type.Name); 257 | Assert.IsNull(parameter1.Type.Namespace); 258 | Assert.AreEqual(0, parameter1.Type.TypeParameters.Count); 259 | 260 | // Second parameter (out param) 261 | var outParameter = testMethod.Parameters[1]; 262 | Assert.AreEqual("outParameter", outParameter.Name); 263 | Assert.AreEqual("Optional", outParameter.Type.Name); 264 | Assert.AreEqual("typing", outParameter.Type.Namespace); 265 | Assert.AreEqual(1, outParameter.Type.TypeParameters.Count); 266 | Assert.AreEqual("str", outParameter.Type.TypeParameters[0].Name); 267 | Assert.IsNull(outParameter.Type.TypeParameters[0].Namespace); 268 | 269 | // Return type 270 | var returnType = testMethod.ReturnType; 271 | Assert.AreEqual("Tuple", returnType.Name); 272 | Assert.AreEqual("typing", returnType.Namespace); 273 | Assert.AreEqual(2, returnType.TypeParameters.Count); 274 | Assert.AreEqual("int", returnType.TypeParameters[0].Name); 275 | Assert.IsNull(returnType.TypeParameters[0].Namespace); 276 | Assert.AreEqual("str", returnType.TypeParameters[1].Name); 277 | Assert.IsNull(returnType.TypeParameters[1].Namespace); 278 | } 279 | 280 | [Test] 281 | public void PropertiesWithProtectedSetter() 282 | { 283 | var testGenerator = new TestGenerator 284 | { 285 | Files = new() 286 | { 287 | { 288 | "Test.cs", 289 | @" 290 | namespace QuantConnect.Test 291 | { 292 | public class TestClass 293 | { 294 | public int TestProperty { get; protected set; } 295 | } 296 | }" 297 | } 298 | } 299 | }; 300 | 301 | var result = testGenerator.GenerateModelsPublic(); 302 | 303 | var namespaces = result.GetNamespaces().ToList(); 304 | Assert.AreEqual(2, namespaces.Count); 305 | 306 | var baseNameSpace = namespaces.Single(x => x.Name == "QuantConnect"); 307 | var testNameSpace = namespaces.Single(x => x.Name == "QuantConnect.Test"); 308 | 309 | var testClass = testNameSpace.GetClasses().Single(); 310 | Assert.AreEqual("TestClass", testClass.Type.Name); 311 | 312 | var testProperty = testClass.Properties.Single(); 313 | Assert.AreEqual("TestProperty", testProperty.Name); 314 | Assert.IsTrue(testProperty.HasSetter); 315 | } 316 | 317 | [Test] 318 | public void CSharpEnumeratorsArePythonIterables() 319 | { 320 | var testGenerator = new TestGenerator 321 | { 322 | Files = new() 323 | { 324 | { 325 | "Test.cs", 326 | @" 327 | using System; 328 | using System.Collections.Generic; 329 | 330 | namespace QuantConnect.Test 331 | { 332 | public class TestEnumerable : IEnumerable 333 | { 334 | public IEnumerator GetEnumerator() 335 | { 336 | yield return 1; 337 | } 338 | IEnumerator IEnumerable.GetEnumerator() 339 | { 340 | return GetEnumerator(); 341 | } 342 | } 343 | }" 344 | } 345 | } 346 | }; 347 | 348 | var result = testGenerator.GenerateModelsPublic(); 349 | 350 | var ns = result.GetNamespaces().Single(x => x.Name == "QuantConnect.Test"); 351 | var classes = ns.GetClasses().ToList(); 352 | 353 | var testEnumerable = classes.Single(x => x.Type.Name == "TestEnumerable"); 354 | var iterableBaseClassType = testEnumerable.InheritsFrom.SingleOrDefault(x => x.Name == "Iterable" && x.Namespace == "typing"); 355 | Assert.IsNotNull(iterableBaseClassType); 356 | Assert.AreEqual(1, iterableBaseClassType.TypeParameters.Count); 357 | var itemType = iterableBaseClassType.TypeParameters[0]; 358 | Assert.AreEqual(new PythonType("int"), itemType); 359 | } 360 | 361 | [Test] 362 | public void IterableOverloadsForEnumerableParameters() 363 | { 364 | var testGenerator = new TestGenerator 365 | { 366 | Files = new() 367 | { 368 | { 369 | "Test.cs", 370 | @" 371 | using System; 372 | using System.Collections.Generic; 373 | 374 | namespace QuantConnect.Test 375 | { 376 | public class TestClass 377 | { 378 | public TestClass(List enumerable) 379 | { 380 | } 381 | 382 | public TestClass(int someParam, IEnumerable enumerable) 383 | { 384 | } 385 | 386 | public void Method1(IList enumerable) 387 | { 388 | } 389 | 390 | public void Method2(IList> enumerable) 391 | { 392 | } 393 | } 394 | }" 395 | } 396 | } 397 | }; 398 | 399 | var result = testGenerator.GenerateModelsPublic(); 400 | 401 | var ns = result.GetNamespaces().Single(x => x.Name == "QuantConnect.Test"); 402 | var classes = ns.GetClasses().ToList(); 403 | 404 | var testClass = classes.Single(x => x.Type.Name == "TestClass"); 405 | 406 | var constructor1 = testClass.Methods.Where(x => x.Name == "__init__" && x.Parameters.Count == 1).Single(); 407 | Assert.AreEqual("typing.List[int]", constructor1.Parameters[0].Type.ToPythonString()); 408 | 409 | var constructor2 = testClass.Methods.Where(x => x.Name == "__init__" && x.Parameters.Count == 2).Single(); 410 | // Enumerable parameters are always converted to List 411 | Assert.AreEqual("typing.List[int]", constructor2.Parameters[1].Type.ToPythonString()); 412 | 413 | var method1 = testClass.Methods.Single(x => x.Name == "Method1"); 414 | Assert.AreEqual("typing.List[str]", method1.Parameters[0].Type.ToPythonString()); 415 | 416 | var method2 = testClass.Methods.Single(x => x.Name == "Method2"); 417 | Assert.AreEqual("typing.List[typing.List[str]]", method2.Parameters[0].Type.ToPythonString()); 418 | } 419 | 420 | [Test] 421 | public void GeneratesSetterForEvents() 422 | { 423 | var testGenerator = new TestGenerator 424 | { 425 | Files = new() 426 | { 427 | { 428 | "Test.cs", 429 | @" 430 | using System; 431 | 432 | namespace QuantConnect.Test 433 | { 434 | public class TestClass 435 | { 436 | private event EventHandler _event; 437 | 438 | public event EventHandler PublicPropertyEvent 439 | { 440 | add { _event += value; } 441 | remove { _event -= value; } 442 | } 443 | 444 | public event EventHandler PublicFieldEvent; 445 | 446 | protected event EventHandler ProtectedPropertyEvent 447 | { 448 | add { _event += value; } 449 | remove { _event -= value; } 450 | } 451 | 452 | protected event EventHandler ProtectedFieldEvent; 453 | 454 | private event EventHandler PrivatePropertyEvent 455 | { 456 | add { _event += value; } 457 | remove { _event -= value; } 458 | } 459 | 460 | private event EventHandler PrivateFieldEvent; 461 | 462 | internal event EventHandler InternalPropertyEvent 463 | { 464 | add { _event += value; } 465 | remove { _event -= value; } 466 | } 467 | 468 | internal event EventHandler InternalFieldEvent; 469 | } 470 | }" 471 | } 472 | } 473 | }; 474 | 475 | var result = testGenerator.GenerateModelsPublic(); 476 | 477 | var ns = result.GetNamespaces().Single(x => x.Name == "QuantConnect.Test"); 478 | var classes = ns.GetClasses().ToList(); 479 | var testClass = classes.Single(x => x.Type.Name == "TestClass"); 480 | 481 | var publicPropertyEvent = testClass.Properties.Single(x => x.Name == "PublicPropertyEvent"); 482 | Assert.IsTrue(publicPropertyEvent.HasSetter); 483 | 484 | var publicFieldEvent = testClass.Properties.Single(x => x.Name == "PublicFieldEvent"); 485 | Assert.IsTrue(publicFieldEvent.HasSetter); 486 | 487 | var protectedPropertyEvent = testClass.Properties.Single(x => x.Name == "ProtectedPropertyEvent"); 488 | Assert.IsTrue(protectedPropertyEvent.HasSetter); 489 | 490 | var protectedFieldEvent = testClass.Properties.Single(x => x.Name == "ProtectedFieldEvent"); 491 | Assert.IsTrue(protectedFieldEvent.HasSetter); 492 | 493 | var privatePropertyEvent = testClass.Properties.SingleOrDefault(x => x.Name == "PrivatePropertyEvent"); 494 | Assert.IsNull(privatePropertyEvent); 495 | 496 | var privateFieldEvent = testClass.Properties.SingleOrDefault(x => x.Name == "PrivateFieldEvent"); 497 | Assert.IsNull(privateFieldEvent); 498 | 499 | var internalPropertyEvent = testClass.Properties.SingleOrDefault(x => x.Name == "InternalPropertyEvent"); 500 | Assert.IsNull(internalPropertyEvent); 501 | 502 | var internalFieldEvent = testClass.Properties.SingleOrDefault(x => x.Name == "InternalFieldEvent"); 503 | Assert.IsNull(internalFieldEvent); 504 | } 505 | 506 | internal class TestGenerator : Generator 507 | { 508 | public Dictionary Files { get; set; } 509 | public TestGenerator() : base("/", "/", "/") 510 | { 511 | } 512 | 513 | protected override IEnumerable GetSyntaxTrees() 514 | { 515 | foreach (var fileContent in Files) 516 | { 517 | yield return CSharpSyntaxTree.ParseText(fileContent.Value, path: fileContent.Key); 518 | } 519 | } 520 | 521 | public ParseContext GenerateModelsPublic() 522 | { 523 | ParseContext context = new(); 524 | 525 | base.GenerateModels(context); 526 | 527 | return context; 528 | } 529 | } 530 | } 531 | } 532 | 533 | --------------------------------------------------------------------------------